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.
This commit is contained in:
Sergio Benitez 2016-03-12 10:45:19 -08:00
parent 967fcd26b2
commit 2e2cc3c216
12 changed files with 403 additions and 115 deletions

2
.gitignore vendored
View File

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

1
README.md Normal file
View File

@ -0,0 +1 @@
# Rocket

View File

@ -0,0 +1,8 @@
[package]
name = "hello"
version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
[dependencies]
rocket = { path = "../../lib" }
rocket_macros = { path = "../../macros" }

View File

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

View File

@ -4,5 +4,4 @@ version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
[dependencies]
hyper = "> 0.7.0"
rocket_macros = { path = "macros" }
hyper = "*"

3
lib/src/error.rs Normal file
View File

@ -0,0 +1,3 @@
pub enum Error {
BadMethod
}

70
lib/src/lib.rs Normal file
View File

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

52
lib/src/method.rs Normal file
View File

@ -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<Method, Error> {
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",
})
}
}

View File

@ -2,11 +2,9 @@
name = "rocket_macros"
version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
description = "Core Rocket Macros"
[dependencies]
hyper = "> 0.0.0"
[lib]
name = "rocket_macros"
plugin = true
[dependencies]
rocket = { path = "../lib/" }

View File

@ -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<P<MetaItem>> = 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());
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
} 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 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,
@ -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<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_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);
}

93
macros/src/macro_utils.rs Normal file
View File

@ -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<T: ToString>(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<T: ToString>(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<MetaItem>])
-> 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<MetaItem>]) -> Option<String> {
// 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
// }

View File

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