From 967fcd26b2a21457224509aecb074004075cddc1 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Mon, 7 Mar 2016 11:28:04 -0800 Subject: [PATCH] Initial commit. Checking for method and path arguments in route. Not storing the info anywhere yet. --- .gitignore | 15 ++++ Cargo.toml | 8 ++ docs/idea.md | 194 ++++++++++++++++++++++++++++++++++++++++++++++ macros/Cargo.toml | 12 +++ macros/src/lib.rs | 134 ++++++++++++++++++++++++++++++++ src/main.rs | 13 ++++ 6 files changed, 376 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 docs/idea.md create mode 100644 macros/Cargo.toml create mode 100644 macros/src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a29d6856 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Compiled files +*.o +*.so +*.rlib +*.dll + +# Executables +*.exe + +# Generated by Cargo +/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 +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..20e732fb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rocket" +version = "0.0.1" +authors = ["Sergio Benitez "] + +[dependencies] +hyper = "> 0.7.0" +rocket_macros = { path = "macros" } diff --git a/docs/idea.md b/docs/idea.md new file mode 100644 index 00000000..6c76b24d --- /dev/null +++ b/docs/idea.md @@ -0,0 +1,194 @@ +# Rust Web Framework + +I really want a nice, easy to use, safe, and stupid fast web framework for +Rust. I don't want a monolithic Rails like thing. I'd much rather have +something that looks like Bottle. That is, use decorators to declare routes. + +Nickel.rs looks kinda nice though (see example 2 below - that looks like, but +isn't Nickel). + +Here's what the simplest program might look like with this framework: + +```rust +#[route("/")] +fn home() -> Response { + Response::string("Hello, world!") +} + +fn main() { + RustWebFramework::run("localhost"); +} +``` + +Alternatively... + +```rust +fn home() -> Response { + Response::string("Hello, world!") +} + +fn main() { + route! { + get '/' => home, + } + + RustWebFramework::run("localhost"); +} +``` + +## Arguments + +Here's what a route that takes arguments might look like: + +```rust +#[route("/")] +fn home(page: &str) -> Response { + Response::string(page) +} +``` + +The really neat thing here is that the `route` macro will typecheck the +function signature. The signature should also have a return type of `Response` +(or whatever the response type ends up being) and take a number of equivalent +to those present in the route. The type of the arguments can be any `T` that +implements `From<&str>`. The conversion will be done automatically by the route +handler. As such, the following will work as expected: + +```rust +#[route("/users/")] +fn home(id: isize) -> Response { + let response_string = format!("User ID: {}", id); + Response::string(response_string) +} +``` + +If the conversion fails, the router should 1) print out a debug error message +and return some user-set no-route-exists things, and 2) allow the programmer to +catch the failure if needed. I'm not quite sure what the best way to allow 2) +is at the moment. Here are a couple of ideas: + + 1. Add an `else` parameter to the `route` macro that will take in the name + of a function to call with the raw string (and more) if the routing + fails: + + #[route("/users/", else = home_failed)] + fn home(id: isize) -> Response { ... } + fn home_failed(route: &str) -> Response { ... } + + 2. Allow the parameter type to be `Result`. Then the route is always + called and the user has to check if the conversion was successful or not. + + 3. Pass it off as an error type to another handler. + +Open questions here: + + 1. What syntax should be used to match a path component to a regular + expression? If for some parameter, call it ``, of type `&str`, we + want to constrain matches to the route to `name`s that match some + regular expression, say `[a-z]+`, how do we specify that? Bottle does: + + + + We can probably just do: + + + + +## Methods + +A different HTTP method can be specified with the `method` `route` argument: + +```rust +#[route(method = POST, "/users")] +fn add_user(name: &str, age: isize) -> Response { ... } +``` + +Or, more succinctly: + +```rust +#[POST("/users")] +fn add_user(name: &str, age: isize) -> Response { ... } +``` + +## Route Priority + +Do we allow two routes to possibly match a single path? Can we even determine +that no two paths conflict given regular expressions? Answer: Yes +(http://arstechnica.com/civis/viewtopic.php?f=20&t=472178). And if so, which +route gets priority? An idea is to add a `priority` parameter to the `route` +macro: + +For example: + +```rust +#[GET("/[name: [a-zA-Z]+", priority = 0)] +#[GET("/[name: [a-z]+", priority = 1)] +``` + +The first route allows lower and uppercase letter, while the second route only +allows lowercase letters. In the case that the entire route has lowercase +letters, the route with the higher priority (1, here) gets called, i.e., the +second one. + +## Error Pages + +There's a route for error pages, too: + +```rust +#[route(method = ERROR, status = 404)] +fn page_not_found(...not sure what goes here yet...) -> Response { .. } +``` + +Or, more succinctly: + +```rust +#[error(404)] +fn page_not_found(...not sure what goes here yet...) -> Response { .. } +``` + +## Open Questions + +1. How is HTTP data handled? (IE, interpret `Content-Type`s) + - Form Data + - JSON: Would be nice to automatically convert to structs. + +2. What about Query Params? + +3. How are cookies handled? + + Presumably you would set them via `Response` and get them via...? + +4. Easy support for (but don't bundle it in...) templating would be nice. + Bottle lets you do: + + #[view("template_name")] + fn hello(...) -> HashMap { .. } + + and automatically instantiates the template `template_name` with the + parameters from the HashMap. + +5. Autoreloading. Maybe use the unix-y reloading thing. Maybe not. + +6. Plugins? Would be nice to easily extend routes. + +7. Pre-post hooks/filters? + +8. Caching? + +9. Session support? + + This is basically a server-side local store identified via an ID in a + cookie. + + http://entrproject.org/ + +10. Environment support? (debug vs. production vs. test, etc.) + +11. Model validation? + +12. Internationalization? + +13. Be faster than https://github.com/julienschmidt/httprouter. + +For 2, 3: the obvious solution is to have a `Request` object with that +information. Do we need that, though? Is there something better? diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 00000000..fc78c1bf --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +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 diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 00000000..e54fc0da --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,134 @@ +#![crate_type = "dylib"] +#![feature(plugin_registrar, rustc_private)] + +extern crate syntax; +extern crate rustc; +extern crate rustc_plugin; +extern crate hyper; + +use std::str::FromStr; + +use rustc_plugin::Registry; +use syntax::parse::token::{intern}; +use syntax::ext::base::SyntaxExtension; +use std::default::Default; + +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; + +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.") +} + +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()); +} + +struct RouteParams { + method: Method, + 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(...)]"), + }; + + 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); + // } + // } + + // `annotated` is the AST object for the annotated item. + let item: &P = match annotated { + &Annotatable::Item(ref item) => item, + &Annotatable::TraitItem(ref item) => bad_item_fatal(ecx, sp, item.span), + &Annotatable::ImplItem(ref item) => bad_item_fatal(ecx, sp, item.span) + }; + + let fn_decl: &P = match item.node { + ItemKind::Fn(ref decl, _, _, _, _, _) => decl, + _ => bad_item_fatal(ecx, sp, item.span) + }; + + println!("Function arguments: {:?}", fn_decl.inputs); +} + +#[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))); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..0f6c41e6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,13 @@ +#![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); +}