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.
This commit is contained in:
Sergio Benitez 2016-03-14 20:43:52 -07:00
parent d5db70afc4
commit ad08fe1d04
10 changed files with 343 additions and 187 deletions

View File

@ -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 = "/<name>")]
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 = "/<x>/<y>")]
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]);
}

View File

@ -1,3 +1,5 @@
pub enum Error {
BadMethod
BadMethod,
BadParse,
NoKey
}

View File

@ -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);
}
}

View File

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

16
lib/src/request.rs Normal file
View File

@ -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<T: FromStr>(&self, name: &str) -> Result<T, Error> {
self.get_param_str(name).and_then(|s| {
T::from_str(s).map_err(|_| Error::BadParse)
})
}
}

20
lib/src/response.rs Normal file
View File

@ -0,0 +1,20 @@
pub struct Response;
impl<'a> From<&'a str> for Response {
fn from(_s: &'a str) -> Self {
Response
}
}
impl From<String> for Response {
fn from(_s: String) -> Self {
Response
}
}
impl Response {
pub fn error(number: usize) -> Response {
println!("ERROR {}!", number);
Response
}
}

View File

@ -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<Item>, &'a P<FnDecl>) {
// `annotated` is the AST object for the annotated item.
let item: &P<Item> = 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<FnDecl> = 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<P<MetaItem>> = 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<Path>> {
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<MacResult + 'static> {
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<Expr> 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"),

View File

@ -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<Item>, &'a P<FnDecl>) {
// `annotated` is the AST object for the annotated item.
let item: &P<Item> = 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<FnDecl> = 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<P<MetaItem>> = 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<Expr> {
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<String> {
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<TokenTree> = 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()));
}

View File

@ -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<Path>> {
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<MacResult + 'static> {
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<Expr> for each &path.
let path_exprs = paths.iter().map(|p| { quote_expr!(ecx, &$p) }).collect();
MacEager::expr(ecx.expr_vec_slice(sp, path_exprs))
}