diff --git a/examples/errors/src/main.rs b/examples/errors/src/main.rs index 263cb7e1..ca5724c2 100644 --- a/examples/errors/src/main.rs +++ b/examples/errors/src/main.rs @@ -4,12 +4,12 @@ extern crate rocket; use rocket::{Rocket, Error, Request}; -#[route(GET, path = "/hello//")] +#[get("/hello//")] fn hello(name: &str, age: i8) -> String { format!("Hello, {} year old named {}!", age, name) } -#[error(code = "404")] +#[error(404)] fn not_found<'r>(_error: Error, request: &'r Request<'r>) -> String { format!("

Sorry, but '{}' is not a valid path!

Try visiting /hello/<name>/<age> instead.

", diff --git a/examples/hello_person/src/main.rs b/examples/hello_person/src/main.rs index adb9e88c..d8660642 100644 --- a/examples/hello_person/src/main.rs +++ b/examples/hello_person/src/main.rs @@ -4,7 +4,7 @@ extern crate rocket; use rocket::Rocket; -#[GET(path = "/hello//")] +#[get("/hello//")] fn hello(name: &str, age: i8) -> String { format!("Hello, {} year old named {}!", age, name) } diff --git a/examples/hello_world/src/main.rs b/examples/hello_world/src/main.rs index f84f8dc2..cf5ce538 100644 --- a/examples/hello_world/src/main.rs +++ b/examples/hello_world/src/main.rs @@ -4,7 +4,7 @@ extern crate rocket; use rocket::Rocket; -#[GET(path = "/")] +#[get("/")] fn root() -> &'static str { "Hello, world!" } diff --git a/lib/src/codegen.rs b/lib/src/codegen.rs index e48dd511..8d9b1563 100644 --- a/lib/src/codegen.rs +++ b/lib/src/codegen.rs @@ -4,7 +4,7 @@ use content_type::ContentType; pub struct StaticRouteInfo { pub method: Method, pub path: &'static str, - pub content_type: ContentType, + pub accept: Option, pub handler: Handler, pub rank: Option, } diff --git a/lib/src/content_type.rs b/lib/src/content_type.rs index 40641b1a..53b14297 100644 --- a/lib/src/content_type.rs +++ b/lib/src/content_type.rs @@ -1,5 +1,6 @@ pub use response::mime::{Mime, TopLevel, SubLevel}; use response::mime::{Param}; +use std::default::Default; use std::str::FromStr; use std::borrow::Borrow; @@ -50,6 +51,13 @@ impl ContentType { is_some!(is_html: Application/Html); } +impl Default for ContentType { + #[inline(always)] + fn default() -> ContentType { + ContentType::any() + } +} + impl Into for ContentType { fn into(self) -> Mime { Mime(self.0, self.1, self.2.unwrap_or_default()) diff --git a/lib/src/router/route.rs b/lib/src/router/route.rs index cb653964..367efb51 100644 --- a/lib/src/router/route.rs +++ b/lib/src/router/route.rs @@ -86,7 +86,7 @@ impl fmt::Display for Route { impl<'a> From<&'a StaticRouteInfo> for Route { fn from(info: &'a StaticRouteInfo) -> Route { let mut route = Route::new(info.method, info.path, info.handler); - route.content_type = info.content_type.clone(); + route.content_type = info.accept.clone().unwrap_or(ContentType::any()); if let Some(rank) = info.rank { route.rank = rank; } diff --git a/macros/src/derive_form.rs b/macros/src/decorators/derive_form.rs similarity index 100% rename from macros/src/derive_form.rs rename to macros/src/decorators/derive_form.rs diff --git a/macros/src/decorators/error.rs b/macros/src/decorators/error.rs new file mode 100644 index 00000000..ac39175d --- /dev/null +++ b/macros/src/decorators/error.rs @@ -0,0 +1,32 @@ +use utils::*; +use ::{CATCH_STRUCT_PREFIX, CATCH_FN_PREFIX}; + +use syntax::codemap::{Span}; +use syntax::ast::{MetaItem}; +use syntax::ext::base::{Annotatable, ExtCtxt}; +use parser::ErrorParams; + +pub fn error_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, + annotated: &Annotatable, push: &mut FnMut(Annotatable)) { + let error = ErrorParams::from(ecx, sp, meta_item, annotated); + + let user_fn_name = error.annotated_fn.ident(); + let catch_fn_name = user_fn_name.prepend(CATCH_FN_PREFIX); + let code = error.code.node; + emit_item(push, quote_item!(ecx, + fn $catch_fn_name<'rocket>(err: ::rocket::Error, + req: &'rocket ::rocket::Request<'rocket>) + -> ::rocket::Response<'rocket> { + rocket::Response::with_raw_status($code, $user_fn_name(err, req)) + } + ).expect("catch function")); + + let struct_name = user_fn_name.prepend(CATCH_STRUCT_PREFIX); + emit_item(push, quote_item!(ecx, + #[allow(non_upper_case_globals)] + pub static $struct_name: rocket::StaticCatchInfo = rocket::StaticCatchInfo { + code: $code, + handler: $catch_fn_name + }; + ).expect("catch info struct")); +} diff --git a/macros/src/decorators/mod.rs b/macros/src/decorators/mod.rs new file mode 100644 index 00000000..3fe3e071 --- /dev/null +++ b/macros/src/decorators/mod.rs @@ -0,0 +1,6 @@ +mod route; +mod error; + +pub use self::route::*; +pub use self::error::*; + diff --git a/macros/src/decorators/route.rs b/macros/src/decorators/route.rs new file mode 100644 index 00000000..7cd7490c --- /dev/null +++ b/macros/src/decorators/route.rs @@ -0,0 +1,199 @@ +use std::collections::HashSet; + +use ::{ROUTE_STRUCT_PREFIX, ROUTE_FN_PREFIX}; +use utils::{emit_item, span, sep_by_tok, SpanExt, IdentExt, ArgExt, option_as_expr}; +use parser::RouteParams; + +use syntax::codemap::{Span, Spanned}; +use syntax::tokenstream::TokenTree; +use syntax::ast::{Arg, Ident, Stmt, Expr, MetaItem, Path}; +use syntax::ext::base::{Annotatable, ExtCtxt}; +use syntax::ext::build::AstBuilder; +use syntax::parse::token::{self, str_to_ident}; +use syntax::ptr::P; + +use rocket::{Method, ContentType}; +use rocket::content_type::{TopLevel, SubLevel}; + +fn method_variant_to_expr(ecx: &ExtCtxt, method: Method) -> Path { + quote_enum!(ecx, method => ::rocket::Method { + Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch; + }) +} + +fn top_level_to_expr(ecx: &ExtCtxt, level: &TopLevel) -> Path { + quote_enum!(ecx, *level => ::rocket::content_type::TopLevel { + Star, Text, Image, Audio, Video, Application, Multipart, Model, Message; + Ext(ref s) => quote_path!(ecx, ::rocket::content_type::TopLevel::Ext($s)) + }) +} + +fn sub_level_to_expr(ecx: &ExtCtxt, level: &SubLevel) -> Path { + quote_enum!(ecx, *level => ::rocket::content_type::SubLevel { + Star, Plain, Html, Xml, Javascript, Css, EventStream, Json, + WwwFormUrlEncoded, Msgpack, OctetStream, FormData, Png, Gif, Bmp, Jpeg; + Ext(ref s) => quote_path!(ecx, ::rocket::content_type::SubLevel::Ext($s)) + }) +} + +fn accept_to_path(ecx: &ExtCtxt, accept: Option) -> Option { + accept.map(|ct| { + let top_level = top_level_to_expr(ecx, &ct.0); + let sub_level = sub_level_to_expr(ecx, &ct.1); + quote_path!(ecx, ::rocket::ContentType($top_level, $sub_level, None)) + }) +} + +trait RouteGenerateExt { + fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option; + fn generate_param_statements(&self, ecx: &ExtCtxt) -> Vec; + fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec; + fn explode(&self, ecx: &ExtCtxt) -> (&String, Path, P, P); +} + +impl RouteGenerateExt for RouteParams { + fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option { + let name = self.form_param.as_ref().map(|p| p.value()); + let arg: &Arg = match name.and_then(|p| self.annotated_fn.find_input(p)) { + Some(arg) => arg, + None => return None + }; + + let (name, ty) = (arg.ident().unwrap(), &arg.ty); + Some(quote_stmt!(ecx, + let $name: $ty = + if let Ok(s) = ::std::str::from_utf8(_req.data.as_slice()) { + if let Ok(v) = ::rocket::form::FromForm::from_form_string(s) { + v + } else { + return ::rocket::Response::not_found(); + } + } else { + return ::rocket::Response::server_error(); + }; + ).expect("form statement")) + } + + // TODO: Add some kind of logging facility in Rocket to get be able to log + // an error/debug message if parsing a parameter fails. + fn generate_param_statements(&self, ecx: &ExtCtxt) -> Vec { + let path_params = self.path_params(ecx); + let all = &self.annotated_fn.decl().inputs; + let declared: HashSet<&str> = path_params.map(|p| p.node).collect(); + let arg_declared = |arg: &&Arg| declared.contains(&*arg.name().unwrap()); + + let mut fn_param_statements = vec![]; + + for (i, arg) in all.iter().filter(&arg_declared).enumerate() { + let (ident, ty) = (arg.ident().unwrap(), &arg.ty); + fn_param_statements.push(quote_stmt!(ecx, + let $ident: $ty = match _req.get_param($i) { + Ok(v) => v, + Err(_) => return ::rocket::Response::forward() + }; + ).expect("declared param parsing statement")); + } + + for arg in all.iter().filter(|p| !arg_declared(p)) { + let (ident, ty) = (arg.ident().unwrap(), &arg.ty); + fn_param_statements.push(quote_stmt!(ecx, + let $ident: $ty = match + <$ty as ::rocket::request::FromRequest>::from_request(&_req) { + Ok(v) => v, + Err(_e) => return ::rocket::Response::forward() + }; + ).expect("undeclared param parsing statement")); + } + + fn_param_statements + } + + fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec { + let args = self.annotated_fn.decl().inputs.iter().map(|a| { + a.ident().expect("function decl pat -> ident").clone() + }).collect::>(); + + sep_by_tok(ecx, &args, token::Comma) + } + + fn explode(&self, ecx: &ExtCtxt) -> (&String, Path, P, P) { + let path = &self.path.node; + let method = method_variant_to_expr(ecx, self.method.node); + let accept = self.accept.as_ref().map(|kv| kv.value().clone()); + let content_type = option_as_expr(ecx, &accept_to_path(ecx, accept)); + let rank = option_as_expr(ecx, &self.rank); + + (path, method, content_type, rank) + } +} + +// FIXME: Compilation fails when parameters have the same name as the function! +fn generic_route_decorator(known_method: Option>, + ecx: &mut ExtCtxt, + sp: Span, + meta_item: &MetaItem, + annotated: &Annotatable, + push: &mut FnMut(Annotatable)) { + // Initialize the logger. + ::rocket::logger::init(::rocket::LoggingLevel::Debug); + + // Parse the route and generate the code to create the form and param vars. + let route = RouteParams::from(ecx, sp, known_method, meta_item, annotated); + let form_statement = route.generate_form_statement(ecx); + let param_statements = route.generate_param_statements(ecx); + let fn_arguments = route.generate_fn_arguments(ecx); + + // Generate and emit the wrapping function with the Rocket handler signature. + let user_fn_name = route.annotated_fn.ident(); + let route_fn_name = user_fn_name.prepend(ROUTE_FN_PREFIX); + emit_item(push, quote_item!(ecx, + fn $route_fn_name<'rocket>(_req: &'rocket ::rocket::Request<'rocket>) + -> ::rocket::Response<'rocket> { + $form_statement + $param_statements + let result = $user_fn_name($fn_arguments); + ::rocket::Response::new(result) + } + ).unwrap()); + + // Generate and emit the static route info that uses the just generated + // function as its handler. A proper Rocket route will be created from this. + let struct_name = user_fn_name.prepend(ROUTE_STRUCT_PREFIX); + let (path, method, content_type, rank) = route.explode(ecx); + emit_item(push, quote_item!(ecx, + #[allow(non_upper_case_globals)] + pub static $struct_name: ::rocket::StaticRouteInfo = + ::rocket::StaticRouteInfo { + method: $method, + path: $path, + handler: $route_fn_name, + accept: $content_type, + rank: $rank, + }; + ).unwrap()); +} + +pub fn route_decorator(ecx: &mut ExtCtxt, + sp: Span, + meta_item: &MetaItem, + annotated: &Annotatable, + push: &mut FnMut(Annotatable)) { + generic_route_decorator(None, ecx, sp, meta_item, annotated, push); +} + +macro_rules! method_decorator { + ($name:ident, $method:ident) => ( + pub fn $name(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, + annotated: &Annotatable, push: &mut FnMut(Annotatable)) { + let i_sp = meta_item.span.shorten_to(meta_item.name().len() as u32); + let method = Some(span(Method::$method, i_sp)); + generic_route_decorator(method, ecx, sp, meta_item, annotated, push); + } + ) +} + +method_decorator!(get_decorator, Get); +method_decorator!(put_decorator, Put); +method_decorator!(post_decorator, Post); +method_decorator!(delete_decorator, Delete); +method_decorator!(patch_decorator, Patch); diff --git a/macros/src/error_decorator.rs b/macros/src/error_decorator.rs deleted file mode 100644 index b6bdb971..00000000 --- a/macros/src/error_decorator.rs +++ /dev/null @@ -1,79 +0,0 @@ -use utils::*; -use meta_item_parser::MetaItemParser; -use super::{CATCH_STRUCT_PREFIX, CATCH_FN_PREFIX}; - -use syntax::codemap::{Span}; -use syntax::ast::{MetaItem}; -use syntax::ext::base::{Annotatable, ExtCtxt}; -use syntax::print::pprust::{item_to_string}; - -#[derive(Debug)] -struct Params { - code: KVSpanned, -} - -fn get_error_params(ecx: &ExtCtxt, meta_item: &MetaItem) -> Params { - // Ensure we've been supplied with a k = v meta item. Error out if not. - let params = meta_item.expect_list(ecx, "Bad use. Expected: #[error(...)]"); - - // Now grab all of the required and optional parameters. - let req: [&'static str; 1] = ["code"]; - let kv_pairs = get_key_values(ecx, meta_item.span, &req, &[], &*params); - - // Ensure we have a code, just to keep parsing and generating errors. - let code = kv_pairs.get("code").map_or(KVSpanned::dummy(404), |c| { - let numeric_code = match c.node.parse() { - Ok(n) => n, - Err(_) => { - let msg = "Error codes must be integer strings. (e.g. \"404\")"; - ecx.span_err(c.v_span, msg); - 404 - } - }; - - if numeric_code < 400 || numeric_code > 599 { - ecx.span_err(c.v_span, "Error codes must be >= 400 and <= 599."); - } - - c.clone().map(|_| { numeric_code }) - }); - - Params { - code: code, - } -} - -pub fn error_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, - annotated: &Annotatable, push: &mut FnMut(Annotatable)) { - let parser = MetaItemParser::new(ecx, meta_item, annotated, &sp); - let item = parser.expect_item(); - - let error_params = get_error_params(ecx, meta_item); - debug!("Error parameters are: {:?}", error_params); - - let fn_name = item.ident; - let catch_fn_name = prepend_ident(CATCH_FN_PREFIX, &item.ident); - let catch_code = error_params.code.node; - let catch_fn_item = quote_item!(ecx, - fn $catch_fn_name<'rocket>(err: ::rocket::Error, - req: &'rocket ::rocket::Request<'rocket>) - -> ::rocket::Response<'rocket> { - // TODO: Figure out what type signature of catcher should be. - let result = $fn_name(err, req); - rocket::Response::with_raw_status($catch_code, result) - } - ).unwrap(); - - debug!("{}", item_to_string(&catch_fn_item)); - push(Annotatable::Item(catch_fn_item)); - - let struct_name = prepend_ident(CATCH_STRUCT_PREFIX, &item.ident); - push(Annotatable::Item(quote_item!(ecx, - #[allow(non_upper_case_globals)] - pub static $struct_name: rocket::StaticCatchInfo = rocket::StaticCatchInfo { - code: $catch_code, - handler: $catch_fn_name - }; - ).unwrap())); -} - diff --git a/macros/src/errors_macro.rs b/macros/src/errors_macro.rs deleted file mode 100644 index 455bfb4b..00000000 --- a/macros/src/errors_macro.rs +++ /dev/null @@ -1,33 +0,0 @@ -use super::{CATCH_STRUCT_PREFIX}; -use utils::*; -use syntax::codemap::Span; -use syntax::tokenstream::TokenTree; -use syntax::ast::Expr; -use syntax::ext::base::{ExtCtxt, MacResult, MacEager}; -use syntax::parse::token::Token; -use syntax::ptr::P; - -#[allow(dead_code)] -const DEBUG: bool = false; - -pub fn errors_macro(ecx: &mut ExtCtxt, _sp: Span, args: &[TokenTree]) - -> Box { - let mut parser = ecx.new_parser_from_tts(args); - let mut paths = parse_paths(&mut parser).unwrap_or_else(|mut e| { - e.emit(); - vec![] - }); - - // Prefix each path terminator - prefix_paths(CATCH_STRUCT_PREFIX, &mut paths); - - // Build up the P for each path. - let path_exprs: Vec> = paths.iter().map(|p| { - quote_expr!(ecx, rocket::Catcher::from(&$p)) - }).collect(); - - // Now put them all in one vector and return the thing. - let path_list = token_separate(ecx, &path_exprs, Token::Comma); - let output = quote_expr!(ecx, vec![$path_list]).unwrap(); - MacEager::expr(P(output)) -} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 698def25..67352197 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,5 +1,8 @@ #![crate_type = "dylib"] #![feature(quote, concat_idents, plugin_registrar, rustc_private)] +#![feature(custom_attribute)] +#![feature(dotdot_in_tuple_patterns)] +#![allow(unused_attributes)] #[macro_use] extern crate syntax; #[macro_use] extern crate log; @@ -10,54 +13,41 @@ extern crate rocket; extern crate env_logger; #[macro_use] mod utils; -mod routes_macro; -mod errors_macro; -mod route_decorator; -mod error_decorator; -mod derive_form; -mod meta_item_parser; +mod parser; +mod macros; +mod decorators; use rustc_plugin::Registry; use syntax::ext::base::SyntaxExtension; use syntax::parse::token::intern; -use routes_macro::routes_macro; -use errors_macro::errors_macro; -use route_decorator::*; -use error_decorator::error_decorator; -use derive_form::from_form_derive; - const ROUTE_STRUCT_PREFIX: &'static str = "static_rocket_route_info_for_"; const CATCH_STRUCT_PREFIX: &'static str = "static_rocket_catch_info_for_"; const ROUTE_FN_PREFIX: &'static str = "rocket_route_fn_"; const CATCH_FN_PREFIX: &'static str = "rocket_catch_fn_"; macro_rules! register_decorators { - ($registry:expr, $($name:expr => $func:expr),+) => ( + ($registry:expr, $($name:expr => $func:ident),+) => ( $($registry.register_syntax_extension(intern($name), - SyntaxExtension::MultiDecorator(Box::new($func))); + SyntaxExtension::MultiDecorator(Box::new(decorators::$func))); )+ ) } #[plugin_registrar] pub fn plugin_registrar(reg: &mut Registry) { - reg.register_macro("routes", routes_macro); - reg.register_macro("errors", errors_macro); + reg.register_macro("routes", macros::routes); + reg.register_macro("errors", macros::errors); register_decorators!(reg, - "derive_FromForm" => from_form_derive, - "route" => generic_route_decorator, + // "derive_FromForm" => from_form_derive "error" => error_decorator, - "GET" => get_decorator, - "PUT" => put_decorator, - "POST" => post_decorator, - "DELETE" => delete_decorator, - "OPTIONS" => options_decorator, - "HEAD" => head_decorator, - "TRACE" => trace_decorator, - "CONNECT" => connect_decorator, - "PATCH" => patch_decorator + "route" => route_decorator, + "get" => get_decorator, + "put" => put_decorator, + "post" => post_decorator, + "delete" => delete_decorator, + "patch" => patch_decorator ); } diff --git a/macros/src/macros/mod.rs b/macros/src/macros/mod.rs new file mode 100644 index 00000000..1cf6649d --- /dev/null +++ b/macros/src/macros/mod.rs @@ -0,0 +1,61 @@ +use {ROUTE_STRUCT_PREFIX, CATCH_STRUCT_PREFIX}; +use utils::{sep_by_tok, ParserExt, IdentExt}; + +use syntax::codemap::Span; +use syntax::tokenstream::TokenTree; +use syntax::ast::{Path, Expr}; +use syntax::ext::base::{DummyResult, ExtCtxt, MacResult, MacEager}; +use syntax::parse::token::Token; +use syntax::ptr::P; + +#[inline] +pub fn prefix_paths(prefix: &str, paths: &mut Vec) { + for p in paths { + let last = p.segments.len() - 1; + let last_seg = &mut p.segments[last]; + last_seg.identifier = last_seg.identifier.prepend(prefix); + } +} + +pub fn prefixing_vec_macro(prefix: &str, + mut to_expr: F, + ecx: &mut ExtCtxt, + sp: Span, + args: &[TokenTree]) + -> Box + where F: FnMut(&ExtCtxt, Path) -> P +{ + let mut parser = ecx.new_parser_from_tts(args); + let paths = parser.parse_paths(); + if let Ok(mut paths) = paths { + // Prefix each path terminator and build up the P for each path. + prefix_paths(prefix, &mut paths); + let path_exprs: Vec> = paths.into_iter() + .map(|path| to_expr(ecx, path)) + .collect(); + + // Now put them all in one vector and return the thing. + let path_list = sep_by_tok(ecx, &path_exprs, Token::Comma); + let output = quote_expr!(ecx, vec![$path_list]).unwrap(); + MacEager::expr(P(output)) + } else { + paths.unwrap_err().emit(); + DummyResult::expr(sp) + } +} + +#[rustfmt_skip] +pub fn routes(ecx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) + -> Box { + prefixing_vec_macro(ROUTE_STRUCT_PREFIX, |ecx, path| { + quote_expr!(ecx, ::rocket::Route::from(&$path)) + }, ecx, sp, args) +} + +#[rustfmt_skip] +pub fn errors(ecx: &mut ExtCtxt, sp: Span, args: &[TokenTree]) + -> Box { + prefixing_vec_macro(CATCH_STRUCT_PREFIX, |ecx, path| { + quote_expr!(ecx, rocket::Catcher::from(&$path)) + }, ecx, sp, args) +} diff --git a/macros/src/parser/error.rs b/macros/src/parser/error.rs new file mode 100644 index 00000000..dbf45cac --- /dev/null +++ b/macros/src/parser/error.rs @@ -0,0 +1,78 @@ +use syntax::ast::*; +use syntax::ext::base::{ExtCtxt, Annotatable}; +use syntax::codemap::{Span, Spanned, dummy_spanned}; + +use utils::{span, MetaItemExt}; +use super::Function; + +/// This structure represents the parsed `error` attribute. +pub struct ErrorParams { + pub annotated_fn: Function, + pub code: Spanned, +} + +impl ErrorParams { + /// Parses the route attribute from the given decorator context. If the + /// parse is not successful, this function exits early with the appropriate + /// error message to the user. + pub fn from(ecx: &mut ExtCtxt, + sp: Span, + meta_item: &MetaItem, + annotated: &Annotatable) + -> ErrorParams { + let function = Function::from(annotated).unwrap_or_else(|item_sp| { + ecx.span_err(sp, "this attribute can only be used on functions..."); + ecx.span_fatal(item_sp, "...but was applied to the item above."); + }); + + let meta_items = meta_item.meta_item_list().unwrap_or_else(|| { + ecx.struct_span_fatal(sp, "incorrect use of attribute") + .help("attributes in Rocket must have the form: #[name(...)]") + .emit(); + unreachable!() + }); + + if meta_items.len() < 1 { + ecx.span_fatal(sp, "attribute requires the `code` parameter"); + } else if meta_items.len() > 1 { + ecx.span_fatal(sp, "attribute can only have one `code` parameter"); + } + + ErrorParams { + annotated_fn: function, + code: parse_code(ecx, &meta_items[0]) + } + } +} + +fn parse_code(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned { + let code_from_u64 = |n: Spanned| { + if n.node < 400 || n.node > 599 { + ecx.span_err(n.span, "code must be >= 400 and <= 599."); + span(0, n.span) + } else { + span(n.node as u16, n.span) + } + }; + + let sp = meta_item.span(); + if let Some((name, lit)) = meta_item.name_value() { + if name != "code" { + ecx.span_err(sp, "the first key, if any, must be 'code'"); + } else if let LitKind::Int(n, _) = lit.node { + return code_from_u64(span(n, lit.span)) + } else { + ecx.span_err(lit.span, "`code` value must be an integer") + } + } else if let Some(n) = meta_item.int_lit() { + return code_from_u64(span(n, sp)) + } else { + ecx.struct_span_err(sp, r#"expected `code = int` or an integer literal"#) + .help(r#"you can specify the code directly as an integer, + e.g: #[error(404)], or as a key-value pair, + e.g: $[error(code = 404)]"#) + .emit(); + } + + dummy_spanned(0) +} diff --git a/macros/src/parser/function.rs b/macros/src/parser/function.rs new file mode 100644 index 00000000..906649de --- /dev/null +++ b/macros/src/parser/function.rs @@ -0,0 +1,36 @@ +use syntax::ast::*; +use syntax::codemap::{Span, Spanned}; +use syntax::ext::base::Annotatable; +use utils::{ArgExt, span}; + +pub struct Function(Spanned<(Ident, FnDecl)>); + +impl Function { + pub fn from(annotated: &Annotatable) -> Result { + let inner = match *annotated { + Annotatable::Item(ref item) => match item.node { + ItemKind::Fn(ref decl, ..) => { + span((item.ident, decl.clone().unwrap()), item.span) + } + _ => return Err(item.span) + }, + Annotatable::TraitItem(ref item) => return Err(item.span), + Annotatable::ImplItem(ref item) => return Err(item.span), + }; + + Ok(Function(inner)) + } + + pub fn ident(&self) -> &Ident { + &self.0.node.0 + } + + pub fn decl(&self) -> &FnDecl { + &self.0.node.1 + } + + pub fn find_input<'a>(&'a self, name: &str) -> Option<&'a Arg> { + self.decl().inputs.iter().filter(|arg| arg.named(name)).next() + } +} + diff --git a/macros/src/parser/keyvalue.rs b/macros/src/parser/keyvalue.rs new file mode 100644 index 00000000..7d2e58df --- /dev/null +++ b/macros/src/parser/keyvalue.rs @@ -0,0 +1,70 @@ +use syntax::codemap::{Spanned, Span, dummy_spanned, DUMMY_SP}; +use syntax::ext::base::ExtCtxt; +use syntax::tokenstream::TokenTree; +use syntax::ext::quote::rt::ToTokens; +use utils::span; + +/// A spanned key-value pair in an attribute. +#[derive(Debug, Clone)] +pub struct KVSpanned { + /// Span for the full key/value pair. + pub span: Span, + /// The spanned key. + pub key: Spanned, + /// The spanned value. + pub value: Spanned +} + +impl KVSpanned { + /// Maps the inner value of this span using `f`. The key, value, and full + /// spans will remain the same in the new KVSpan. + pub fn map U>(self, f: F) -> KVSpanned { + KVSpanned { + span: self.span, + key: self.key, + value: span(f(self.value.node), self.value.span) + } + } + + /// Maps a reference of the inner value of this span using `f`. The key, + /// value, and full spans will remain the same in the new KVSpan. + pub fn map_ref U>(&self, f: F) -> KVSpanned { + KVSpanned { + span: self.span, + key: self.key.clone(), + value: span(f(&self.value.node), self.value.span) + } + } + + /// Returns the unspanned key. Purely for convenience. + pub fn key(&self) -> &String { + &self.key.node + } + + /// Returns the unspanned value. Purely for convenience. + pub fn value(&self) -> &V { + &self.value.node + } +} + +impl Default for KVSpanned { + /// Returns a dummy KVSpan with an empty key for the default value of T. + fn default() -> KVSpanned { + dummy_kvspanned("", T::default()) + } +} + +impl ToTokens for KVSpanned { + fn to_tokens(&self, cx: &ExtCtxt) -> Vec { + self.value().to_tokens(cx) + } +} + +/// Creates a KVSpanned value with dummy (meaningless) spans. +pub fn dummy_kvspanned(key: S, value: T) -> KVSpanned { + KVSpanned { + span: DUMMY_SP, + key: dummy_spanned(key.to_string()), + value: dummy_spanned(value), + } +} diff --git a/macros/src/meta_item_parser.rs b/macros/src/parser/meta_item_parser.rs similarity index 98% rename from macros/src/meta_item_parser.rs rename to macros/src/parser/meta_item_parser.rs index 5880bd4f..5a08fe04 100644 --- a/macros/src/meta_item_parser.rs +++ b/macros/src/parser/meta_item_parser.rs @@ -131,8 +131,8 @@ pub trait RouteDecoratorExt { impl<'a, 'c> RouteDecoratorExt for MetaItemParser<'a, 'c> { fn bad_method(&self, sp: Span, message: &str) { - let message = format!("{} Valid methods are: [GET, PUT, POST, DELETE, \ - OPTIONS, HEAD, TRACE, CONNECT, PATCH]", message); + let message = format!("{} {}", message, + "Valid methods are: [GET, PUT, POST, DELETE, PATCH]"); self.ctxt.span_err(sp, message.as_str()); } diff --git a/macros/src/parser/mod.rs b/macros/src/parser/mod.rs new file mode 100644 index 00000000..e923ed2c --- /dev/null +++ b/macros/src/parser/mod.rs @@ -0,0 +1,11 @@ +mod keyvalue; +mod route; +mod error; +mod param; +mod function; + +pub use self::keyvalue::KVSpanned; +pub use self::route::RouteParams; +pub use self::error::ErrorParams; +pub use self::param::ParamIter; +pub use self::function::Function; diff --git a/macros/src/parser/param.rs b/macros/src/parser/param.rs new file mode 100644 index 00000000..1642602c --- /dev/null +++ b/macros/src/parser/param.rs @@ -0,0 +1,58 @@ +use syntax::ext::base::ExtCtxt; +use syntax::codemap::{Span, Spanned, BytePos}; + +use utils::span; + +pub struct ParamIter<'s, 'a, 'c: 'a> { + ctxt: &'a ExtCtxt<'c>, + span: Span, + string: &'s str +} + +impl<'s, 'a, 'c: 'a> ParamIter<'s, 'a, 'c> { + pub fn new(c: &'a ExtCtxt<'c>, s: &'s str, p: Span) -> ParamIter<'s, 'a, 'c> { + ParamIter { + ctxt: c, + span: p, + string: s + } + } +} + +impl<'s, 'a, 'c> Iterator for ParamIter<'s, 'a, 'c> { + type Item = Spanned<&'s str>; + + fn next(&mut self) -> Option> { + // Find the start and end indexes for the next parameter, if any. + let (start, end) = match (self.string.find('<'), self.string.find('>')) { + (Some(i), Some(j)) => (i, j), + _ => return None + }; + + // Ensure we found a valid parameter. + if end <= start { + self.ctxt.span_err(self.span, "Parameter list is malformed."); + return None; + } + + // Calculate the parameter and the span for the parameter. + let param = &self.string[(start + 1)..end]; + let mut param_span = self.span; + param_span.lo = self.span.lo + BytePos(start as u32); + param_span.hi = self.span.lo + BytePos((end + 1) as u32); + + // Check for nonemptiness and that the characters are correct. + if param.is_empty() { + self.ctxt.span_err(param_span, "Parameter names cannot be empty."); + None + } else if param.contains(|c: char| !c.is_alphanumeric()) { + self.ctxt.span_err(param_span, "Parameters must be alphanumeric."); + None + } else { + self.string = &self.string[(end + 1)..]; + self.span.lo = self.span.lo + BytePos((end + 1) as u32); + Some(span(param, param_span)) + } + } +} + diff --git a/macros/src/parser/route.rs b/macros/src/parser/route.rs new file mode 100644 index 00000000..d986cbb8 --- /dev/null +++ b/macros/src/parser/route.rs @@ -0,0 +1,280 @@ +use std::str::FromStr; +use std::collections::HashSet; + +use syntax::ast::*; +use syntax::ext::base::{ExtCtxt, Annotatable}; +use syntax::codemap::{Span, Spanned, dummy_spanned}; + +use utils::{span, MetaItemExt, SpanExt, ArgExt}; +use super::ParamIter; +use super::keyvalue::KVSpanned; +use rocket::{Method, ContentType}; + +pub struct Function(Spanned<(Ident, FnDecl)>); + +impl Function { + fn from(annotated: &Annotatable) -> Result { + let inner = match *annotated { + Annotatable::Item(ref item) => match item.node { + ItemKind::Fn(ref decl, ..) => { + span((item.ident, decl.clone().unwrap()), item.span) + } + _ => return Err(item.span) + }, + Annotatable::TraitItem(ref item) => return Err(item.span), + Annotatable::ImplItem(ref item) => return Err(item.span), + }; + + Ok(Function(inner)) + } + + pub fn ident(&self) -> &Ident { + &self.0.node.0 + } + + pub fn decl(&self) -> &FnDecl { + &self.0.node.1 + } + + pub fn find_input<'a>(&'a self, name: &str) -> Option<&'a Arg> { + self.decl().inputs.iter().filter(|arg| arg.named(name)).next() + } +} + +/// This structure represents the parsed `route` attribute. +/// +/// It contains all of the information supplied by the user and the span where +/// the user supplied the information. This structure can only be obtained by +/// calling the `RouteParams::from` function and passing in the entire decorator +/// environment. +pub struct RouteParams { + pub annotated_fn: Function, + pub method: Spanned, + pub path: Spanned, + pub form_param: Option>, + pub accept: Option>, + pub rank: Option>, +} + +impl RouteParams { + /// Parses the route attribute from the given decorator context. If the + /// parse is not successful, this function exits early with the appropriate + /// error message to the user. + pub fn from(ecx: &mut ExtCtxt, + sp: Span, + known_method: Option>, + meta_item: &MetaItem, + annotated: &Annotatable) + -> RouteParams { + let function = Function::from(annotated).unwrap_or_else(|item_sp| { + ecx.span_err(sp, "this attribute can only be used on functions..."); + ecx.span_fatal(item_sp, "...but was applied to the item above."); + }); + + let meta_items = meta_item.meta_item_list().unwrap_or_else(|| { + ecx.struct_span_fatal(sp, "incorrect use of attribute") + .help("attributes in Rocket must have the form: #[name(...)]") + .emit(); + unreachable!() + }); + + if meta_items.len() < 1 { + ecx.span_fatal(sp, "attribute requires at least 1 parameter"); + } + + // Figure out the method. If it is known (i.e, because we're parsing a + // helper attribute), use that method directly. Otherwise, try to parse + // it from the list of meta items. + let (method, attr_params) = match known_method { + Some(method) => (method, meta_items), + None => (parse_method(ecx, &meta_items[0]), &meta_items[1..]) + }; + + if attr_params.len() < 1 { + ecx.struct_span_fatal(sp, "attribute requires at least a path") + .help(r#"example: #[get("/my/path")] or #[get(path = "/hi")]"#) + .emit(); + unreachable!() + } + + let path = parse_path(ecx, &attr_params[0]); + + // Parse all of the optional parameters. + // TODO: Factor this out for use in Error. + let mut seen_keys = HashSet::new(); + let (mut rank, mut form, mut accept) = Default::default(); + for param in &attr_params[1..] { + let kv_opt = kv_from_nested(¶m); + if kv_opt.is_none() { + ecx.span_err(param.span(), "expected key = value"); + continue; + } + + 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), + "accept" => accept = parse_opt(ecx, &kv, parse_accept), + _ => { + let msg = format!("{} is not a known parameter", kv.key()); + ecx.span_err(kv.span, &msg); + continue; + } + } + + if seen_keys.contains(kv.key()) { + let msg = format!("{} was already defined", kv.key()); + ecx.struct_span_warn(param.span, &msg) + .note("the last declared value will be used") + .emit(); + } else { + seen_keys.insert(kv.key().clone()); + } + } + + RouteParams { + method: method, + path: path, + form_param: form, + accept: accept, + rank: rank, + annotated_fn: function, + } + } + + pub fn path_params<'s, 'a, 'c: 'a>(&'s self, + ecx: &'a ExtCtxt<'c>) + -> ParamIter<'s, 'a, 'c> { + ParamIter::new(ecx, self.path.node.as_str(), self.path.span) + } +} + +fn is_valid_method(method: Method) -> bool { + use rocket::Method::*; + match method { + Get | Put | Post | Delete | Patch => true, + _ => false + } +} + +pub fn kv_from_nested(item: &NestedMetaItem) -> Option> { + item.name_value().map(|(name, value)| { + let k_span = item.span().shorten_to(name.len() as u32); + KVSpanned { + key: span(name.to_string(), k_span), + value: value.clone(), + span: item.span(), + } + }) +} + +fn parse_method(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned { + if let Some(word) = meta_item.word() { + if let Ok(method) = Method::from_str(&*word.name()) { + if is_valid_method(method) { + return span(method, word.span()); + } + } else { + let msg = format!("{} is not a valid method.", word.name()); + ecx.span_err(word.span(), &msg); + } + } + + // Fallthrough. Return default method. + ecx.struct_span_err(meta_item.span, "expected a valid HTTP method") + .help("valid methods are: GET, PUT, POST, DELETE, PATCH") + .emit(); + + return dummy_spanned(Method::Get); +} + +fn parse_path(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned { + let sp = meta_item.span(); + if let Some((name, lit)) = meta_item.name_value() { + if name != "path" { + ecx.span_err(sp, "the first key, if any, must be 'path'"); + } else if let LitKind::Str(ref s, _) = lit.node { + return span(s.to_string(), lit.span); + } else { + ecx.span_err(lit.span, "`path` value must be a string") + } + } else if let Some(s) = meta_item.str_lit() { + return span(s.to_string(), sp); + } else { + ecx.struct_span_err(sp, r#"expected `path = string` or a path string"#) + .help(r#"you can specify the path directly as a string, \ + e.g: "/hello/world", or as a key-value pair, \ + e.g: path = "/hello/world" "#) + .emit(); + } + + dummy_spanned("".to_string()) +} + +fn parse_opt(ecx: &ExtCtxt, kv: &KVSpanned, f: F) -> Option> + where F: Fn(&ExtCtxt, &KVSpanned) -> O +{ + Some(kv.map_ref(|_| f(ecx, kv))) +} + +fn parse_form(ecx: &ExtCtxt, kv: &KVSpanned) -> String { + if let LitKind::Str(ref s, _) = *kv.value() { + if s.starts_with('<') && s.ends_with('>') { + let form_param = s[1..(s.len() - 1)].to_string(); + if form_param.chars().all(char::is_alphanumeric) { + return form_param; + } + + ecx.span_err(kv.value.span, "parameter name must be alphanumeric"); + } + } else { + ecx.struct_span_err(kv.span, r#"expected `form = ""`"#) + .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 = """#) + .emit(); + } + + "".to_string() +} + +fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned) -> isize { + if let LitKind::Int(n, _) = *kv.value() { + let max = isize::max_value(); + if n <= max as u64 { + return n as isize; + } else { + let msg = format!("rank must be less than or equal to {}", max); + ecx.span_err(kv.value.span, msg.as_str()); + } + } else { + ecx.struct_span_err(kv.span, r#"expected `rank = int`"#) + .help(r#"the rank, if specified, must be a key-value pair where + the key is `rank` and the value is an integer. + e.g: rank = 1, or e.g: rank = 10"#) + .emit(); + } + + -1 +} + +fn parse_accept(ecx: &ExtCtxt, kv: &KVSpanned) -> ContentType { + if let LitKind::Str(ref s, _) = *kv.value() { + if let Ok(ct) = ContentType::from_str(s) { + if ct.is_ext() { + let msg = format!("'{}' is not a known content-type", s); + ecx.span_warn(kv.value.span, &msg); + } else { + return ct; + } + } + } else { + ecx.struct_span_err(kv.span, r#"expected `accept = "content/type"`"#) + .help(r#"accept, if specified, must be a key-value pair where + the key is `accept` and the value is a string representing the + content-type accepted. e.g: accept = "application/json""#) + .emit(); + } + + ContentType::any() +} diff --git a/macros/src/route_decorator.rs b/macros/src/route_decorator.rs deleted file mode 100644 index 5363e2fa..00000000 --- a/macros/src/route_decorator.rs +++ /dev/null @@ -1,314 +0,0 @@ -use super::{ROUTE_STRUCT_PREFIX, ROUTE_FN_PREFIX}; -use utils::*; -use meta_item_parser::{MetaItemParser, RouteDecoratorExt}; - -use std::collections::HashMap; - -use syntax::codemap::{Span, BytePos, /* DUMMY_SP, */ Spanned}; -use syntax::ast::{Stmt, Expr, MetaItem, FnDecl}; -use syntax::ext::base::{Annotatable, ExtCtxt}; -use syntax::ptr::P; -use syntax::print::pprust::{item_to_string, stmt_to_string}; -use syntax::parse::token::{self, str_to_ident}; - -use rocket::{Method, ContentType}; -use rocket::content_type::{TopLevel, SubLevel}; - -pub fn extract_params_from_kv<'a>(parser: &MetaItemParser, - params: &'a KVSpanned) -> Vec> { - let mut param_span = params.v_span; - param_span.lo = params.v_span.lo + BytePos(1); - let spanned = span(&*params.node, param_span); - parser.iter_params(&spanned).collect() -} - -// Analyzes the declared parameters against the function declaration. Returns -// a vector of all of the parameters in the order the user wants them. -fn get_fn_params<'a, T: Iterator>>(ecx: &ExtCtxt, - declared_params: T, fn_decl: &Spanned<&FnDecl>) - -> Vec { - debug!("FUNCTION: {:?}", fn_decl); - - // First, check that all of the parameters are unique. - let mut seen: HashMap<&str, &Spanned<&str>> = HashMap::new(); - for item in declared_params { - if seen.contains_key(item.node) { - let msg = format!( - "\"{}\" was declared as a parameter more than once.", item.node); - ecx.span_err(item.span, msg.as_str()); - } else { - seen.insert(item.node, item); - } - } - - let mut user_params = vec![]; - - // Ensure every param in the function declaration was declared by the user. - for arg in &fn_decl.node.inputs { - let name = arg.pat.expect_ident(ecx, "Expected identifier."); - let arg = SimpleArg::new(name, arg.ty.clone(), arg.pat.span); - if seen.remove(&*name.to_string()).is_some() { - user_params.push(UserParam::new(arg, true)); - } else { - user_params.push(UserParam::new(arg, false)); - } - } - - // Emit an error on every attribute param that didn't match in fn params. - for item in seen.values() { - let msg = format!("'{}' was declared in the attribute...", item.node); - ecx.span_err(item.span, msg.as_str()); - ecx.span_err(fn_decl.span, "...but does not appear in the function \ - declaration."); - } - - user_params -} - -fn get_form_stmt(ecx: &ExtCtxt, fn_args: &mut Vec, - form_params: &[Spanned<&str>]) -> Option { - if form_params.len() < 1 { - return None; - } else if form_params.len() > 1 { - panic!("Allowed more than 1 form parameter!"); - } - - let param_name = &form_params[0].node; - let (param_ty, param_ident) = { - // 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. - // We should have already caught this, so just return None. - return None; - } - - (fn_arg.unwrap().ty.clone(), str_to_ident(param_name)) - }; - - // Remove the paramter from the function arguments. - 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); - - // The actual code we'll be inserting. - quote_stmt!(ecx, - let $param_ident: $param_ty = - if let Ok(form_string) = ::std::str::from_utf8(_req.data.as_slice()) { - match ::rocket::form::FromForm::from_form_string(form_string) { - Ok(v) => v, - Err(_) => { - // TODO: - // debug!("\t=> Form failed to parse."); - return ::rocket::Response::not_found(); - } - } - } else { - return ::rocket::Response::server_error(); - } - ) -} - -// TODO: 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), - } -} - -// Same here. -fn top_level_to_expr(ecx: &ExtCtxt, level: &TopLevel) -> P { - use rocket::content_type::TopLevel::*; - match *level { - Star => quote_expr!(ecx, ::rocket::content_type::TopLevel::Star), - Text => quote_expr!(ecx, ::rocket::content_type::TopLevel::Text), - Image => quote_expr!(ecx, ::rocket::content_type::TopLevel::Image), - Audio => quote_expr!(ecx, ::rocket::content_type::TopLevel::Audio), - Video => quote_expr!(ecx, ::rocket::content_type::TopLevel::Video), - Application => quote_expr!(ecx, ::rocket::content_type::TopLevel::Application), - Multipart => quote_expr!(ecx, ::rocket::content_type::TopLevel::Multipart), - Message => quote_expr!(ecx, ::rocket::content_type::TopLevel::Message), - Model => quote_expr!(ecx, ::rocket::content_type::TopLevel::Model), - Ext(ref s) => quote_expr!(ecx, ::rocket::content_type::TopLevel::Ext($s)), - } -} - -// Same here. -fn sub_level_to_expr(ecx: &ExtCtxt, level: &SubLevel) -> P { - use rocket::content_type::SubLevel::*; - match *level { - Star => quote_expr!(ecx, ::rocket::content_type::SubLevel::Star), - Plain => quote_expr!(ecx, ::rocket::content_type::SubLevel::Plain), - Html => quote_expr!(ecx, ::rocket::content_type::SubLevel::Html), - Xml => quote_expr!(ecx, ::rocket::content_type::SubLevel::Xml), - Javascript => quote_expr!(ecx, ::rocket::content_type::SubLevel::Javascript), - Css => quote_expr!(ecx, ::rocket::content_type::SubLevel::Css), - EventStream => quote_expr!(ecx, ::rocket::content_type::SubLevel::EventStream), - Json => quote_expr!(ecx, ::rocket::content_type::SubLevel::Json), - WwwFormUrlEncoded => - quote_expr!(ecx, ::rocket::content_type::SubLevel::WwwFormUrlEncoded), - Msgpack => quote_expr!(ecx, ::rocket::content_type::SubLevel::Msgpack), - OctetStream => - quote_expr!(ecx, ::rocket::content_type::SubLevel::OctetStream), - FormData => quote_expr!(ecx, ::rocket::content_type::SubLevel::FormData), - Png => quote_expr!(ecx, ::rocket::content_type::SubLevel::Png), - Gif => quote_expr!(ecx, ::rocket::content_type::SubLevel::Gif), - Bmp => quote_expr!(ecx, ::rocket::content_type::SubLevel::Bmp), - Jpeg => quote_expr!(ecx, ::rocket::content_type::SubLevel::Jpeg), - Ext(ref s) => quote_expr!(ecx, ::rocket::content_type::SubLevel::Ext($s)), - } -} - -fn content_type_to_expr(ecx: &ExtCtxt, content_type: &ContentType) -> P { - let top_level = top_level_to_expr(ecx, &content_type.0); - let sub_level = sub_level_to_expr(ecx, &content_type.1); - quote_expr!(ecx, ::rocket::ContentType($top_level, $sub_level, None)) -} - -// FIXME: Compilation fails when parameters have the same name as the function! -pub fn route_decorator(known_method: Option>, ecx: &mut ExtCtxt, - sp: Span, meta_item: &MetaItem, annotated: &Annotatable, - push: &mut FnMut(Annotatable)) { - ::rocket::logger::init(::rocket::LoggingLevel::Debug); - - // Get the encompassing item and function declaration for the annotated func. - let parser = MetaItemParser::new(ecx, meta_item, annotated, &sp); - let (item, fn_decl) = (parser.expect_item(), parser.expect_fn_decl()); - - // Parse and retrieve all of the parameters of the route. - let route = parser.parse_route(known_method); - - // Get a list of the user declared parameters in `path` and `form`. - let path_params = extract_params_from_kv(&parser, &route.path); - let form_thing = route.form.unwrap_or_default(); // Default is empty string. - let form_params = extract_params_from_kv(&parser, &form_thing); - - // Ensure the params match the function declaration and return the params. - let all_params = path_params.iter().chain(form_params.iter()); - let mut user_params = get_fn_params(ecx, all_params, &fn_decl); - - // 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 fn_param_idents = token_separate(ecx, &user_params, token::Comma); - - // Generate the statements that will attempt to parse forms during run-time. - // Calling this function also remove the form parameter from fn_params. - let form_stmt = get_form_stmt(ecx, &mut user_params, &form_params); - form_stmt.as_ref().map(|s| debug!("Form stmt: {:?}", stmt_to_string(s))); - - // Generate the statements that will parse paramaters during run-time. - let mut fn_param_exprs = vec![]; - - // Push all of the declared parameters. - for (i, param) in user_params.iter().filter(|p| p.declared).enumerate() { - let (ident, ty) = (str_to_ident(param.as_str()), ¶m.ty); - let fn_item = quote_stmt!(ecx, - let $ident: $ty = match _req.get_param($i) { - Ok(v) => v, - Err(_) => return ::rocket::Response::forward() - }; - ).unwrap(); - - debug!("Declared FN: {}", stmt_to_string(&fn_item)); - fn_param_exprs.push(fn_item); - } - - // Push all of the undeclared (FromRequest) parameters. - for param in user_params.iter().filter(|p| !p.declared) { - let (ident, ty) = (str_to_ident(param.as_str()), ¶m.ty); - let fn_item = quote_stmt!(ecx, - let $ident: $ty = match - <$ty as ::rocket::request::FromRequest>::from_request(&_req) { - Ok(v) => v, - Err(_e) => { - // TODO: Add $ident and $ty to the string. - // TODO: Add some kind of loggin facility in Rocket - // to get the formatting right (IE, so it idents - // correctly). - // debug!("Failed to parse: {:?}", e); - return ::rocket::Response::forward(); - } - }; - ).unwrap(); - - debug!("Param FN: {}", stmt_to_string(&fn_item)); - fn_param_exprs.push(fn_item); - } - - let route_fn_name = prepend_ident(ROUTE_FN_PREFIX, &item.ident); - let fn_name = item.ident; - let route_fn_item = quote_item!(ecx, - fn $route_fn_name<'rocket>(_req: &'rocket ::rocket::Request<'rocket>) - -> ::rocket::Response<'rocket> { - $form_stmt - $fn_param_exprs - let result = $fn_name($fn_param_idents); - ::rocket::Response::new(result) - } - ).unwrap(); - - debug!("{}", item_to_string(&route_fn_item)); - push(Annotatable::Item(route_fn_item)); - - let struct_name = prepend_ident(ROUTE_STRUCT_PREFIX, &item.ident); - let path = &route.path.node; - let method = method_variant_to_expr(ecx, route.method.node); - let content_type = content_type_to_expr(ecx, &route.content_type.node); - let rank = option_as_expr(ecx, &route.rank); - - let static_item = quote_item!(ecx, - #[allow(non_upper_case_globals)] - pub static $struct_name: ::rocket::StaticRouteInfo = - ::rocket::StaticRouteInfo { - method: $method, - path: $path, - handler: $route_fn_name, - content_type: $content_type, - rank: $rank, - }; - ).unwrap(); - - debug!("Emitting static: {}", item_to_string(&static_item)); - push(Annotatable::Item(static_item)); -} - - -pub fn generic_route_decorator(ecx: &mut ExtCtxt, - sp: Span, meta_item: &MetaItem, annotated: &Annotatable, - push: &mut FnMut(Annotatable)) { - route_decorator(None, ecx, sp, meta_item, annotated, push); -} - -macro_rules! method_decorator { - ($name:ident, $method:ident) => ( - pub fn $name(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, - annotated: &Annotatable, push: &mut FnMut(Annotatable)) { - let mut i_sp = meta_item.span; - i_sp.hi = i_sp.lo + BytePos(meta_item.name().len() as u32); - let method = Some(span(Method::$method, i_sp)); - route_decorator(method, ecx, sp, meta_item, annotated, push); - } - ) -} - -method_decorator!(get_decorator, Get); -method_decorator!(put_decorator, Put); -method_decorator!(post_decorator, Post); -method_decorator!(delete_decorator, Delete); -method_decorator!(options_decorator, Options); -method_decorator!(head_decorator, Head); -method_decorator!(trace_decorator, Trace); -method_decorator!(connect_decorator, Connect); -method_decorator!(patch_decorator, Patch); diff --git a/macros/src/routes_macro.rs b/macros/src/routes_macro.rs deleted file mode 100644 index c357ca67..00000000 --- a/macros/src/routes_macro.rs +++ /dev/null @@ -1,33 +0,0 @@ -use super::{ROUTE_STRUCT_PREFIX}; -use utils::*; -use syntax::codemap::Span; -use syntax::tokenstream::TokenTree; -use syntax::ast::Expr; -use syntax::ext::base::{ExtCtxt, MacResult, MacEager}; -use syntax::parse::token::Token; -use syntax::ptr::P; - -#[allow(dead_code)] -const DEBUG: bool = false; - -pub fn routes_macro(ecx: &mut ExtCtxt, _sp: Span, args: &[TokenTree]) - -> Box { - let mut parser = ecx.new_parser_from_tts(args); - let mut paths = parse_paths(&mut parser).unwrap_or_else(|mut e| { - e.emit(); - vec![] - }); - - // Prefix each path terminator. - prefix_paths(ROUTE_STRUCT_PREFIX, &mut paths); - - // Build up the P for each path. - let path_exprs: Vec> = paths.iter().map(|p| { - quote_expr!(ecx, ::rocket::Route::from(&$p)) - }).collect(); - - // Now put them all in one vector and return the thing. - let path_list = token_separate(ecx, &path_exprs, Token::Comma); - let output = quote_expr!(ecx, vec![$path_list]).unwrap(); - MacEager::expr(P(output)) -} diff --git a/macros/src/utils.rs b/macros/src/utils.rs deleted file mode 100644 index 8846ba4d..00000000 --- a/macros/src/utils.rs +++ /dev/null @@ -1,287 +0,0 @@ -use std::ops::Deref; - -use syntax::parse::{token}; -use syntax::parse::token::Token; -use syntax::tokenstream::TokenTree; -use syntax::ast::{Path, Expr, Ident, MetaItem, MetaItemKind, LitKind, Ty, self}; -use syntax::ext::base::{ExtCtxt}; -use syntax::codemap::{Span, Spanned, BytePos, DUMMY_SP}; -use syntax::ext::quote::rt::ToTokens; -use syntax::parse::PResult; -use syntax::parse::parser::{PathStyle, Parser}; -use syntax::ptr::P; - -use std::collections::{HashSet, HashMap}; - -pub fn prepend_ident(other: T, ident: &Ident) -> Ident { - let mut new_ident = other.to_string(); - new_ident.push_str(ident.name.to_string().as_str()); - token::str_to_ident(new_ident.as_str()) -} - -#[allow(dead_code)] -pub fn append_ident(ident: &Ident, other: T) -> Ident { - let mut new_ident = ident.name.to_string(); - new_ident.push_str(other.to_string().as_str()); - token::str_to_ident(new_ident.as_str()) -} - -#[inline] -pub fn 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, - } -} - -#[derive(Debug, Clone)] -pub struct KVSpanned { - pub k_span: Span, // Span for the key. - pub v_span: Span, // Span for the value. - pub p_span: Span, // Span for the full parameter. - pub node: T // The value. -} - -impl KVSpanned { - #[inline] - pub fn dummy(t: T) -> KVSpanned { - KVSpanned { - k_span: DUMMY_SP, - v_span: DUMMY_SP, - p_span: DUMMY_SP, - node: t, - } - } -} - -impl Default for KVSpanned { - fn default() -> KVSpanned { - KVSpanned::dummy(T::default()) - } -} - -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: &ExtCtxt, sp: Span, required: &[&str], - optional: &[&str], kv_params: &'b [P]) - -> HashMap<&'b str, KVSpanned<&'b str>> { - let mut seen = HashSet::new(); - let mut kv_pairs = HashMap::new(); - - // Collect all the kv pairs, keeping track of what we've seen. - for param in kv_params { - if let MetaItemKind::NameValue(ref name, ref value) = param.node { - if required.contains(&&**name) || optional.contains(&&**name) { - if seen.contains(&**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 { - 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(value.span, "Value must be a string."); - } - } else { - let msg = format!("'{}' is not a valid parameter.", &**name); - ecx.span_err(param.span, &msg); - } - } else { - ecx.span_err(param.span, "Expected 'key = value', found:"); - } - } - - // Now, trigger an error for missing `required` params. - for req in required { - if !seen.contains(req) { - let m = format!("'{}' parameter is required but is missing.", req); - ecx.span_err(sp, &m); - } - } - - kv_pairs -} - -pub fn token_separate(ecx: &ExtCtxt, things: &[T], - token: Token) -> Vec { - let mut output: Vec = vec![]; - for (i, thing) in things.iter().enumerate() { - output.extend(thing.to_tokens(ecx)); - if i < things.len() - 1 { - output.push(TokenTree::Token(DUMMY_SP, token.clone())); - } - } - - output -} - -pub trait MetaItemExt { - fn expect_list<'a>(&'a self, ecx: &ExtCtxt, msg: &str) -> &'a Vec>; - fn expect_word<'a>(&'a self, ecx: &ExtCtxt, msg: &str) -> &'a str; - fn is_word(&self) -> bool; - fn name(&self) -> &str; -} - -impl MetaItemExt for MetaItem { - fn name(&self) -> &str { - let interned_name = match self.node { - MetaItemKind::Word(ref s) | MetaItemKind::List(ref s, _) - | MetaItemKind::NameValue(ref s, _) => s, - }; - - &*interned_name - } - - fn expect_list<'a>(&'a self, ecx: &ExtCtxt, msg: &str) -> &'a Vec> { - match self.node { - MetaItemKind::List(_, ref params) => params, - _ => ecx.span_fatal(self.span, msg) - } - } - - fn expect_word<'a>(&'a self, ecx: &ExtCtxt, msg: &str) -> &'a str { - match self.node { - MetaItemKind::Word(ref s) => &*s, - _ => ecx.span_fatal(self.span, msg) - } - } - - fn is_word(&self) -> bool { - match self.node { - MetaItemKind::Word(_) => true, - _ => false - } - } -} - -pub trait PatExt { - fn expect_ident<'a>(&'a self, ecx: &ExtCtxt, msg: &str) -> &'a Ident; -} - -impl PatExt for ast::Pat { - fn expect_ident<'a>(&'a self, ecx: &ExtCtxt, msg: &str) -> &'a Ident { - match self.node { - ast::PatKind::Ident(_, ref ident, _) => &ident.node, - _ => { - ecx.span_fatal(self.span, msg) - } - } - } -} - -pub fn parse_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(PathStyle::Mod))); - if !parser.eat(&Token::Comma) { - try!(parser.expect(&Token::Eof)); - break; - } - } - - Ok(results) -} - -pub fn prefix_paths(prefix: &str, paths: &mut Vec) { - for p in paths { - let last = p.segments.len() - 1; - let last_seg = &mut p.segments[last]; - let new_ident = prepend_ident(prefix, &last_seg.identifier); - last_seg.identifier = new_ident; - } -} - -pub fn option_as_expr(ecx: &ExtCtxt, opt: &Option) -> P { - match *opt { - Some(ref item) => quote_expr!(ecx, Some($item)), - None => quote_expr!(ecx, None) - } -} - -#[derive(Debug)] -pub struct SimpleArg { - pub name: String, - pub ty: P, - pub span: Span -} - -impl SimpleArg { - pub fn new(name: T, ty: P, sp: Span) -> SimpleArg { - SimpleArg { name: name.to_string(), ty: ty, span: sp } - } - - pub fn as_str(&self) -> &str { - self.name.as_str() - } -} - -impl ToTokens for SimpleArg { - fn to_tokens(&self, cx: &ExtCtxt) -> Vec { - token::str_to_ident(self.as_str()).to_tokens(cx) - } -} - -pub struct UserParam { - pub arg: SimpleArg, - pub declared: bool -} - -impl UserParam { - pub fn new(arg: SimpleArg, declared: bool) -> UserParam { - UserParam { - arg: arg, - declared: declared - } - } -} - -impl Deref for UserParam { - type Target = SimpleArg; - - fn deref(&self) -> &SimpleArg { - &self.arg - } -} - -impl ToTokens for UserParam { - fn to_tokens(&self, cx: &ExtCtxt) -> Vec { - self.arg.to_tokens(cx) - } -} - diff --git a/macros/src/utils/arg_ext.rs b/macros/src/utils/arg_ext.rs new file mode 100644 index 00000000..5a460e00 --- /dev/null +++ b/macros/src/utils/arg_ext.rs @@ -0,0 +1,24 @@ +use syntax::ast::{Arg, PatKind, Ident}; + +pub trait ArgExt { + fn ident(&self) -> Option<&Ident>; + + fn name(&self) -> Option { + self.ident().map(|ident| { + ident.name.to_string() + }) + } + + fn named(&self, name: &str) -> bool { + self.name().map_or(false, |a| a == name) + } +} + +impl ArgExt for Arg { + fn ident(&self) -> Option<&Ident> { + match self.pat.node { + PatKind::Ident(_, ref ident, _) => Some(&ident.node), + _ => None, + } + } +} diff --git a/macros/src/utils/ident_ext.rs b/macros/src/utils/ident_ext.rs new file mode 100644 index 00000000..5f65d3a6 --- /dev/null +++ b/macros/src/utils/ident_ext.rs @@ -0,0 +1,20 @@ +use std::fmt::Display; +use syntax::parse::token::str_to_ident; +use syntax::ast::Ident; + +pub trait IdentExt { + fn prepend(&self, other: T) -> Ident; + fn append(&self, other: T) -> Ident; +} + +impl IdentExt for Ident { + fn prepend(&self, other: T) -> Ident { + let new_ident = format!("{}{}", other, self.name); + str_to_ident(new_ident.as_str()) + } + + fn append(&self, other: T) -> Ident { + let new_ident = format!("{}{}", self.name, other); + str_to_ident(new_ident.as_str()) + } +} diff --git a/macros/src/utils/meta_item_ext.rs b/macros/src/utils/meta_item_ext.rs new file mode 100644 index 00000000..a407c0c7 --- /dev/null +++ b/macros/src/utils/meta_item_ext.rs @@ -0,0 +1,31 @@ +use syntax::ast::{LitKind, NestedMetaItem, MetaItemKind, Lit}; +use syntax::parse::token::InternedString; + +pub trait MetaItemExt { + fn name_value(&self) -> Option<(&InternedString, &Lit)>; + fn str_lit(&self) -> Option<&InternedString>; + fn int_lit(&self) -> Option; +} + +impl MetaItemExt for NestedMetaItem { + fn name_value(&self) -> Option<(&InternedString, &Lit)> { + self.meta_item().and_then(|mi| match mi.node { + MetaItemKind::NameValue(ref s, ref l) => Some((s, l)), + _ => None, + }) + } + + fn str_lit(&self) -> Option<&InternedString> { + self.literal().and_then(|lit| match lit.node { + LitKind::Str(ref s, _) => Some(s), + _ => None, + }) + } + + fn int_lit(&self) -> Option { + self.literal().and_then(|lit| match lit.node { + LitKind::Int(n, _) => Some(n), + _ => None, + }) + } +} diff --git a/macros/src/utils/mod.rs b/macros/src/utils/mod.rs new file mode 100644 index 00000000..28802963 --- /dev/null +++ b/macros/src/utils/mod.rs @@ -0,0 +1,74 @@ +mod meta_item_ext; +mod arg_ext; +mod parser_ext; +mod ident_ext; +mod span_ext; + +pub use self::arg_ext::ArgExt; +pub use self::meta_item_ext::MetaItemExt; +pub use self::parser_ext::ParserExt; +pub use self::ident_ext::IdentExt; +pub use self::span_ext::SpanExt; + +use syntax::parse::token::Token; +use syntax::tokenstream::TokenTree; +use syntax::ast::{Item, Expr}; +use syntax::ext::base::{Annotatable, ExtCtxt}; +use syntax::codemap::{spanned, Span, Spanned, DUMMY_SP}; +use syntax::ext::quote::rt::ToTokens; +use syntax::print::pprust::item_to_string; +use syntax::ptr::P; + +#[inline] +pub fn span(t: T, span: Span) -> Spanned { + spanned(span.lo, span.hi, t) +} + +#[inline] +pub fn sep_by_tok(ecx: &ExtCtxt, things: &[T], token: Token) -> Vec + where T: ToTokens +{ + let mut output: Vec = vec![]; + for (i, thing) in things.iter().enumerate() { + output.extend(thing.to_tokens(ecx)); + if i < things.len() - 1 { + output.push(TokenTree::Token(DUMMY_SP, token.clone())); + } + } + + output +} + +#[inline] +pub fn option_as_expr(ecx: &ExtCtxt, opt: &Option) -> P { + match *opt { + Some(ref item) => quote_expr!(ecx, Some($item)), + None => quote_expr!(ecx, None), + } +} + +#[inline] +pub fn emit_item(push: &mut FnMut(Annotatable), item: P) { + debug!("Emitting item: {}", item_to_string(&item)); + push(Annotatable::Item(item)); +} + +#[macro_export] +macro_rules! quote_enum { + ($ecx:expr, $var:expr => $(::$root:ident)+ + { $($variant:ident),+ ; $($extra:pat => $result:expr),* }) => ({ + use syntax::codemap::DUMMY_SP; + use $(::$root)+::*; + let root_idents = vec![$(str_to_ident(stringify!($root))),+]; + match $var { + $($variant => { + let variant = str_to_ident(stringify!($variant)); + let mut idents = root_idents.clone(); + idents.push(variant); + $ecx.path_global(DUMMY_SP, idents) + })+ + $($extra => $result)* + } + }) +} + diff --git a/macros/src/utils/parser_ext.rs b/macros/src/utils/parser_ext.rs new file mode 100644 index 00000000..44f18fa1 --- /dev/null +++ b/macros/src/utils/parser_ext.rs @@ -0,0 +1,17 @@ +use syntax::parse::parser::{PathStyle, Parser}; +use syntax::parse::PResult; +use syntax::ast::Path; +use syntax::parse::token::Token::{Eof, Comma}; +use syntax::parse::common::SeqSep; + +pub trait ParserExt<'a> { + fn parse_paths(&mut self) -> PResult<'a, Vec>; +} + +impl<'a> ParserExt<'a> for Parser<'a> { + fn parse_paths(&mut self) -> PResult<'a, Vec> { + self.parse_seq_to_end(&Eof, + SeqSep::trailing_allowed(Comma), + |p| p.parse_path(PathStyle::Mod)) + } +} diff --git a/macros/src/utils/pat_ext.rs b/macros/src/utils/pat_ext.rs new file mode 100644 index 00000000..beb7f5d8 --- /dev/null +++ b/macros/src/utils/pat_ext.rs @@ -0,0 +1,28 @@ +use syntax::ast::{Pat, PatKind, Ident}; +use syntax::parse::token; +use syntax::codemap::DUMMY_SP; +use syntax::tokenstream::TokenTree; +use syntax::ext::quote::rt::ToTokens; +use syntax::ext::base::ExtCtxt; +use syntax::ptr::P; + +pub trait PatExt { + fn named(&self, name: &str) -> bool; + fn ident(&self) -> Option<&Ident>; +} + +impl PatExt for Pat { + fn named(&self, name: &str) -> bool { + match self.node { + PatKind::Ident(_, ref ident, _) => ident.node.name.as_str() == name, + _ => false, + } + } + + fn ident(&self) -> Option<&Ident> { + match self.node { + PatKind::Ident(_, ref ident, _) => Some(&ident.node), + _ => None, + } + } +} diff --git a/macros/src/utils/span_ext.rs b/macros/src/utils/span_ext.rs new file mode 100644 index 00000000..0722755a --- /dev/null +++ b/macros/src/utils/span_ext.rs @@ -0,0 +1,12 @@ +use syntax::codemap::{Span, BytePos}; + +pub trait SpanExt { + fn shorten_to(self, to_length: u32) -> Span; +} + +impl SpanExt for Span { + fn shorten_to(mut self, to_length: u32) -> Span { + self.hi = self.lo + BytePos(to_length); + self + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..d2add6b7 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +enum_trailing_comma = false +max_width = 85 +fn_call_width = 80