From 2e2cc3c21631a6c834f61b9184effd7ef6c2deef Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sat, 12 Mar 2016 10:45:19 -0800 Subject: [PATCH] Rocket is almost operational! `routes!` macro complete. Here's what works so far: * The `route` decorator checks its inputs correctly. There's a nice utility for doing this, and it's working quite well at the moment. * The `route` decorator emits a `route_fn` and a `route_struct`. The `routes` * macro prepends the path terminator with the route struct prefix. The * `Rocket` library can read mount information (though not act on it properly just yet) and launch a server using Hyper. --- .gitignore | 2 +- README.md | 1 + examples/hello/Cargo.toml | 8 ++ examples/hello/src/main.rs | 25 ++++ Cargo.toml => lib/Cargo.toml | 3 +- lib/src/error.rs | 3 + lib/src/lib.rs | 70 ++++++++++ lib/src/method.rs | 52 ++++++++ macros/Cargo.toml | 8 +- macros/src/lib.rs | 240 +++++++++++++++++++++-------------- macros/src/macro_utils.rs | 93 ++++++++++++++ src/main.rs | 13 -- 12 files changed, 403 insertions(+), 115 deletions(-) create mode 100644 README.md create mode 100644 examples/hello/Cargo.toml create mode 100644 examples/hello/src/main.rs rename Cargo.toml => lib/Cargo.toml (65%) create mode 100644 lib/src/error.rs create mode 100644 lib/src/lib.rs create mode 100644 lib/src/method.rs create mode 100644 macros/src/macro_utils.rs delete mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore index a29d6856..fe9df4b5 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ *.exe # Generated by Cargo -/target/ +target # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock diff --git a/README.md b/README.md new file mode 100644 index 00000000..30d572d0 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Rocket diff --git a/examples/hello/Cargo.toml b/examples/hello/Cargo.toml new file mode 100644 index 00000000..06d53b5a --- /dev/null +++ b/examples/hello/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello" +version = "0.0.1" +authors = ["Sergio Benitez "] + +[dependencies] +rocket = { path = "../../lib" } +rocket_macros = { path = "../../macros" } diff --git a/examples/hello/src/main.rs b/examples/hello/src/main.rs new file mode 100644 index 00000000..a1fb27f2 --- /dev/null +++ b/examples/hello/src/main.rs @@ -0,0 +1,25 @@ +#![feature(plugin)] +#![plugin(rocket_macros)] + +extern crate rocket; +use rocket::{Rocket, Request, Response, Method, Route}; + +#[route(GET, path = "/hello")] +fn hello() -> &'static str { + "Hello, world!" +} + +mod test { + use rocket::{Request, Response, Method, Route}; + + #[route(GET, path = "")] + pub fn hello() -> &'static str { + "Hello, world!" + } +} + +fn main() { + let mut rocket = Rocket::new("localhost", 8000); + rocket.mount("/test", routes![test::hello]); + rocket.mount_and_launch("/", routes![hello]); +} diff --git a/Cargo.toml b/lib/Cargo.toml similarity index 65% rename from Cargo.toml rename to lib/Cargo.toml index 20e732fb..7c75dbcc 100644 --- a/Cargo.toml +++ b/lib/Cargo.toml @@ -4,5 +4,4 @@ version = "0.0.1" authors = ["Sergio Benitez "] [dependencies] -hyper = "> 0.7.0" -rocket_macros = { path = "macros" } +hyper = "*" diff --git a/lib/src/error.rs b/lib/src/error.rs new file mode 100644 index 00000000..6343403b --- /dev/null +++ b/lib/src/error.rs @@ -0,0 +1,3 @@ +pub enum Error { + BadMethod +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 00000000..1e1d4299 --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,70 @@ +extern crate hyper; + +mod method; +mod error; + +pub use method::Method; +pub use error::Error; + +use hyper::server::Handler as HypHandler; +use hyper::server::Request as HypRequest; +use hyper::server::Response as HypResponse; +use hyper::net::Fresh as HypFresh; +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, + pub path: &'a str, + pub handler: Handler +} + +#[allow(dead_code)] +pub struct Rocket { + address: &'static str, + port: isize, + // mounts: HashMap<&'static str, Route<'a>> +} + +impl HypHandler for Rocket { + fn handle<'a, 'k>(&'a self, req: HypRequest<'a, 'k>, + res: HypResponse<'a, HypFresh>) { + println!("Request: {:?}", req.uri); + res.send(b"Hello World!").unwrap(); + } +} + +impl Rocket { + pub fn new(address: &'static str, port: isize) -> Rocket { + Rocket { + address: address, + port: port + } + } + + pub fn mount(&mut self, base: &str, routes: &[&Route]) -> &mut Self { + println!("Mounting at {}", base); + for route in routes { + println!(" - Found {} route to {}", route.method, route.path); + (route.handler)(Request); + } + + self + } + + pub fn mount_and_launch(mut self, base: &str, routes: &[&Route]) { + self.mount(base, routes); + self.launch(); + } + + pub fn launch(self) { + let full_addr = format!("{}:{}", self.address, self.port); + println!("🚀 Rocket is launching ({})...", full_addr); + let _ = Server::http(full_addr.as_str()).unwrap().handle(self); + } +} diff --git a/lib/src/method.rs b/lib/src/method.rs new file mode 100644 index 00000000..b1a62985 --- /dev/null +++ b/lib/src/method.rs @@ -0,0 +1,52 @@ +use self::Method::{Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch}; +use std::str::FromStr; +use std::fmt::{self, Display}; +use error::Error; + +#[derive(Clone, PartialEq, Eq, Hash, Debug)] +pub enum Method { + Get, + Put, + Post, + Delete, + Options, + Head, + Trace, + Connect, + Patch +} + +impl FromStr for Method { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "OPTIONS" => Ok(Options), + "GET" => Ok(Get), + "POST" => Ok(Post), + "PUT" => Ok(Put), + "DELETE" => Ok(Delete), + "HEAD" => Ok(Head), + "TRACE" => Ok(Trace), + "CONNECT" => Ok(Connect), + "PATCH" => Ok(Patch), + _ => Err(Error::BadMethod) + } + } +} + +impl Display for Method { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.write_str(match *self { + Options => "OPTIONS", + Get => "GET", + Post => "POST", + Put => "PUT", + Delete => "DELETE", + Head => "HEAD", + Trace => "TRACE", + Connect => "CONNECT", + Patch => "PATCH", + }) + } +} diff --git a/macros/Cargo.toml b/macros/Cargo.toml index fc78c1bf..e66a8b8b 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -2,11 +2,9 @@ name = "rocket_macros" version = "0.0.1" authors = ["Sergio Benitez "] -description = "Core Rocket Macros" - -[dependencies] -hyper = "> 0.0.0" [lib] -name = "rocket_macros" plugin = true + +[dependencies] +rocket = { path = "../lib/" } diff --git a/macros/src/lib.rs b/macros/src/lib.rs index e54fc0da..00076bdc 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,116 +1,59 @@ #![crate_type = "dylib"] -#![feature(plugin_registrar, rustc_private)] +#![feature(quote, concat_idents, plugin_registrar, rustc_private)] -extern crate syntax; +#[macro_use] extern crate syntax; extern crate rustc; extern crate rustc_plugin; -extern crate hyper; +extern crate rocket; + +#[macro_use] mod macro_utils; + +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 std::default::Default; - +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 hyper::method::Method; +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; -fn bad_item_fatal(ecx: &mut ExtCtxt, dec_sp: Span, item_sp: Span) -> ! { - ecx.span_err(dec_sp, "This decorator cannot be used on non-functions..."); - ecx.span_fatal(item_sp, "...but an attempt to use it on the item below was made.") -} +const DEBUG: bool = true; -fn bad_method_err(ecx: &mut ExtCtxt, dec_sp: Span, method: &str) { - let message = format!("`{}` is not a valid method. Valid methods are: \ - [GET, POST, PUT, DELETE, HEAD, PATCH]", method); - ecx.span_err(dec_sp, message.as_str()); -} +const STRUCT_PREFIX: &'static str = "ROCKET_ROUTE_STRUCT_"; +const FN_PREFIX: &'static str = "rocket_route_fn_"; -struct RouteParams { +use rocket::Method; + +struct Params { method: Method, - path: String, + path: String } -fn demo_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, - annotated: &Annotatable, _push: &mut FnMut(Annotatable)) { - // Word: #[demo] - // List: #[demo(one, two, ..)] or #[demo(one = "1", ...)] or mix both - // NameValue: #[demo = "1"] - let params: &Vec> = match meta_item.node { - MetaItemKind::List(_, ref params) => params, - // Would almost certainly be better to use "DummyResult" here. - _ => ecx.span_fatal(meta_item.span, - "incorrect use of macro. correct form is: #[demo(...)]"), - }; +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.") +} - if params.len() < 2 { - ecx.span_fatal(meta_item.span, "Bad invocation. Need >= 2 arguments."); - } - - let (method_param, kv_params) = params.split_first().unwrap(); - let method = if let MetaItemKind::Word(ref word) = method_param.node { - let method = Method::from_str(word).unwrap_or_else(|e| { - Method::Extension(String::from(&**word)) - }); - - if let Method::Extension(ref name) = method { - bad_method_err(ecx, meta_item.span, name.as_str()); - Method::Get - } else { - method - } - } else { - Method::Get - }; - - let mut route_params: RouteParams = RouteParams { - method: method, - path: String::new() - }; - - let mut found_path = false; - for param in kv_params { - if let MetaItemKind::NameValue(ref name, ref value) = param.node { - match &**name { - "path" => { - found_path = true; - if let LitKind::Str(ref string, _) = value.node { - route_params.path = String::from(&**string); - } else { - ecx.span_err(param.span, "Path value must be string."); - } - }, - _ => { - ecx.span_err(param.span, "Unrecognized parameter."); - } - } - - } else { - ecx.span_err(param.span, "Invalid parameter. Must be key = value."); - } - } - - if !found_path { - ecx.span_err(meta_item.span, "`path` argument is missing."); - } - - // for param in params { - // if let MetaItemKind::Word(ref word) = param.node { - // if hyper::method::Method::from_str(word).is_ok() { - // println!("METHOD! {}", word); - // } - - // println!("WORD Param: {:?}", param); - // } else { - // println!("NOT word Param: {:?}", param); - // } - // } +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, @@ -123,12 +66,121 @@ fn demo_decorator(ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, _ => bad_item_fatal(ecx, sp, item.span) }; - println!("Function arguments: {:?}", fn_decl.inputs); + (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_macro("rn", expand_rn); reg.register_syntax_extension(intern("route"), - SyntaxExtension::MultiDecorator(Box::new(demo_decorator))); + SyntaxExtension::MultiDecorator(Box::new(route_decorator))); + reg.register_macro("routes", routes_macro); } diff --git a/macros/src/macro_utils.rs b/macros/src/macro_utils.rs new file mode 100644 index 00000000..3a1ac126 --- /dev/null +++ b/macros/src/macro_utils.rs @@ -0,0 +1,93 @@ +use syntax::parse::{token}; +use syntax::ast::{Ident, MetaItem, MetaItemKind, LitKind}; +use syntax::ext::base::{ExtCtxt}; +use syntax::codemap::Span; +use syntax::ptr::P; + +use std::collections::{HashSet, HashMap}; + +// macro_rules! debug { +// ($session:expr, $span:expr, $($message:tt)*) => ({ +// if cfg!(debug) { +// span_note!($session, $span, "{}:{}", file!(), line!()); +// span_note!($session, $span, $($message)*); +// } +// }) +// } + +macro_rules! debug { + ($($message:tt)*) => ({ + if DEBUG { + println!("{}:{}", file!(), line!()); + println!($($message)*); + } + }) +} + +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()) +} + +pub fn get_key_values<'b>(ecx: &mut ExtCtxt, sp: Span, required: &[&str], + optional: &[&str], kv_params: &'b [P]) + -> HashMap<&'b str, &'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!("'{}' cannot be set 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); + } else { + ecx.span_err(param.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 find_value_for(key: &str, kv_params: &[P]) -> Option { +// for param in kv_params { +// if let MetaItemKind::NameValue(ref name, ref value) = param.node { +// if &**name == key { +// if let LitKind::Str(ref string, _) = value.node { +// return Some(String::from(&**string)); +// } +// } +// } +// } + +// None +// } diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 0f6c41e6..00000000 --- a/src/main.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![feature(plugin)] -#![plugin(rocket_macros)] - -#[route(POST, path = "/")] -fn function(_x: usize, _y: isize) { - -} - -#[route(GET, path = "/")] -fn main() { - println!("Hello, world!"); - function(1, 2); -}