diff --git a/Cargo.toml b/Cargo.toml index 22c3b6b3..55ed61ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = [ "examples/extended_validation", "examples/forms", "examples/hello_person", - "examples/hello_query_params", + "examples/query_params", "examples/hello_world", "examples/manual_routes", "examples/optional_redirect", diff --git a/examples/hello_query_params/src/main.rs b/examples/hello_query_params/src/main.rs deleted file mode 100644 index ad7d2031..00000000 --- a/examples/hello_query_params/src/main.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![feature(plugin)] -#![plugin(rocket_macros)] - -extern crate rocket; - -use rocket::{Rocket, Error}; - -// One idea of what we could get. -// #[route(GET, path = "/hello?{name,age}")] -// fn hello(name: &str, age: &str) -> String { -// "Hello!".to_string() -// // format!("Hello, {} year old named {}!", age, name) -// } - -// Another idea. -// #[route(GET, path = "/hello")] -// fn hello(q: QueryParams) -> IOResult { -// format!("Hello, {} year old named {}!", q.get("name")?, q.get("age")?) -// } - -#[get("/hello")] -fn hello() -> &'static str { - "Hello there! Don't have query params yet, but we're working on it." -} - -fn main() { - Rocket::new("localhost", 8000).mount_and_launch("/", routes![hello]); -} diff --git a/examples/hello_query_params/Cargo.toml b/examples/query_params/Cargo.toml similarity index 89% rename from examples/hello_query_params/Cargo.toml rename to examples/query_params/Cargo.toml index 88669264..a01dd888 100644 --- a/examples/hello_query_params/Cargo.toml +++ b/examples/query_params/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "hello_query" +name = "query_params" version = "0.0.1" authors = ["Sergio Benitez "] workspace = "../../" diff --git a/examples/query_params/src/main.rs b/examples/query_params/src/main.rs new file mode 100644 index 00000000..6824d6ce --- /dev/null +++ b/examples/query_params/src/main.rs @@ -0,0 +1,25 @@ +#![feature(plugin, custom_derive)] +#![plugin(rocket_macros)] + +extern crate rocket; + +use rocket::{Rocket, Error}; + +#[derive(FromForm)] +struct Person<'r> { + name: &'r str, + age: Option +} + +#[get("/hello?")] +fn hello(person: Person) -> String { + if let Some(age) = person.age { + format!("Hello, {} year old named {}!", age, person.name) + } else { + format!("Hello {}!", person.name) + } +} + +fn main() { + Rocket::new("localhost", 8000).mount_and_launch("/", routes![hello]); +} diff --git a/lib/src/form.rs b/lib/src/form.rs index fbb43c0a..46fb8666 100644 --- a/lib/src/form.rs +++ b/lib/src/form.rs @@ -13,9 +13,9 @@ pub trait FromFormValue<'v>: Sized { fn parse(v: &'v str) -> Result; - // Returns a default value to be used when the form field does not exist. If - // this returns None, then the field is required. Otherwise, this should - // return Some(default_value). + /// Returns a default value to be used when the form field does not exist. + /// If this returns None, then the field is required. Otherwise, this should + /// return Some(default_value). fn default() -> Option { None } @@ -95,27 +95,37 @@ impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result { } } -pub fn form_items<'f>(string: &'f str, items: &mut [(&'f str, &'f str)]) -> usize { - let mut param_num = 0; - let mut rest = string; - while !rest.is_empty() && param_num < items.len() { - let (key, remainder) = match rest.find('=') { - Some(index) => (&rest[..index], &rest[(index + 1)..]), - None => return param_num +pub struct FormItems<'f>(pub &'f str); + +impl<'f> Iterator for FormItems<'f> { + type Item = (&'f str, &'f str); + + fn next(&mut self) -> Option { + let string = self.0; + let (key, rest) = match string.find('=') { + Some(index) => (&string[..index], &string[(index + 1)..]), + None => return None }; - rest = remainder; let (value, remainder) = match rest.find('&') { Some(index) => (&rest[..index], &rest[(index + 1)..]), None => (rest, "") }; - rest = remainder; - items[param_num] = (key, value); - param_num += 1; + self.0 = remainder; + Some((key, value)) + } +} + + +pub fn form_items<'f>(string: &'f str, items: &mut [(&'f str, &'f str)]) -> usize { + let mut param_count = 0; + for (i, item) in FormItems(string).take(items.len()).enumerate() { + items[i] = item; + param_count += 1; } - param_num + param_count } #[cfg(test)] diff --git a/lib/src/router/uri.rs b/lib/src/router/uri.rs index 85f3ef22..ba8b8581 100644 --- a/lib/src/router/uri.rs +++ b/lib/src/router/uri.rs @@ -16,7 +16,7 @@ impl<'a> URI<'a> { let uri = uri.as_ref(); let (path, query) = match uri.find('?') { - Some(index) => (&uri[..index], Some(&uri[index..])), + Some(index) => (&uri[..index], Some(&uri[(index + 1)..])), None => (uri, None) }; @@ -40,6 +40,10 @@ impl<'a> URI<'a> { Segments(self.path) } + pub fn query(&self) -> Option<&'a str> { + self.query + } + pub fn as_str(&self) -> &'a str { self.uri } diff --git a/macros/src/decorators/route.rs b/macros/src/decorators/route.rs index 14ecf9f8..68eb0abf 100644 --- a/macros/src/decorators/route.rs +++ b/macros/src/decorators/route.rs @@ -1,4 +1,5 @@ use std::collections::HashSet; +use std::fmt::Display; use ::{ROUTE_STRUCT_PREFIX, ROUTE_FN_PREFIX, PARAM_PREFIX}; use utils::{emit_item, span, sep_by_tok, SpanExt, IdentExt, ArgExt, option_as_expr}; @@ -6,7 +7,7 @@ use parser::RouteParams; use syntax::codemap::{Span, Spanned}; use syntax::tokenstream::TokenTree; -use syntax::ast::{Arg, Ident, Stmt, Expr, MetaItem, Path}; +use syntax::ast::{Name, 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}; @@ -21,6 +22,7 @@ fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path { }) } +// FIXME: This should return an Expr! (Ext is not a path.) 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; @@ -28,6 +30,7 @@ fn top_level_to_expr(ecx: &ExtCtxt, level: &TopLevel) -> Path { }) } +// FIXME: This should return an Expr! (Ext is not a path.) 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, @@ -45,24 +48,31 @@ fn content_type_to_expr(ecx: &ExtCtxt, ct: Option) -> Option>, P) -> Option; + fn missing_declared_err(&self, ecx: &ExtCtxt, arg: &Spanned); + fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option; + fn generate_query_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 param = self.form_param.as_ref(); - let arg = param.and_then(|p| self.annotated_fn.find_input(p.value())); + fn missing_declared_err(&self, ecx: &ExtCtxt, arg: &Spanned) { + let fn_span = self.annotated_fn.span(); + let msg = format!("'{}' is declared as an argument...", arg.node); + ecx.span_err(arg.span, &msg); + ecx.span_err(fn_span, "...but isn't in the function signature."); + } + + fn gen_form(&self, ecx: &ExtCtxt, param: Option<&Spanned>, + form_string: P) -> Option { + let arg = param.and_then(|p| self.annotated_fn.find_input(&p.node.name)); if param.is_none() { return None; } else if arg.is_none() { - let param = param.unwrap(); - let fn_span = self.annotated_fn.span(); - let msg = format!("'{}' is declared as an argument...", param.value()); - ecx.span_err(param.span, &msg); - ecx.span_err(fn_span, "...but isn't in the function signature."); + self.missing_declared_err(ecx, ¶m.unwrap()); return None; } @@ -70,18 +80,38 @@ impl RouteGenerateExt for RouteParams { let (name, ty) = (arg.ident().unwrap().prepend(PARAM_PREFIX), &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(); + match ::rocket::form::FromForm::from_form_string($form_string) { + Ok(v) => v, + Err(_) => return ::rocket::Response::forward() }; ).expect("form statement")) } + fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option { + let param = self.form_param.as_ref().map(|p| &p.value); + let expr = quote_expr!(ecx, + match ::std::str::from_utf8(_req.data.as_slice()) { + Ok(s) => s, + Err(_) => return ::rocket::Response::server_error() + } + ); + + self.gen_form(ecx, param, expr) + } + + fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option { + let param = self.query_param.as_ref(); + let expr = quote_expr!(ecx, + match _req.uri().query() { + // FIXME: Don't reinterpret as UTF8 again. + Some(query) => query, + None => return ::rocket::Response::forward() + } + ); + + self.gen_form(ecx, param, expr) + } + // 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 { @@ -91,16 +121,13 @@ impl RouteGenerateExt for RouteParams { // Retrieve an iterator over the user's path parameters and ensure that // each parameter appears in the function signature. for param in ¶ms { - if self.annotated_fn.find_input(param.node).is_none() { - let fn_span = self.annotated_fn.span(); - let msg = format!("'{}' is declared as an argument...", param.node); - ecx.span_err(param.span, &msg); - ecx.span_err(fn_span, "...but isn't in the function signature."); + if self.annotated_fn.find_input(¶m.node.name).is_none() { + self.missing_declared_err(ecx, ¶m); } } // Create a function thats checks if an argument was declared in `path`. - let set: HashSet<&str> = params.iter().map(|p| p.node).collect(); + let set: HashSet<&Name> = params.iter().map(|p| &p.node.name).collect(); let declared = &|arg: &&Arg| set.contains(&*arg.name().unwrap()); // These are all of the arguments in the function signature. @@ -117,11 +144,13 @@ impl RouteGenerateExt for RouteParams { ).expect("declared param parsing statement")); } - // A from_request parameter is one that isnt't declared and isn't `form`. + // A from_request parameter is one that isn't declared, `form`, or query. let from_request = |a: &&Arg| { - let a_name = &*a.name().unwrap(); - !declared(a) - && self.form_param.as_ref().map_or(true, |p| p.value() != a_name) + !declared(a) && self.form_param.as_ref().map_or(true, |p| { + !a.named(&p.value().name) + }) && self.query_param.as_ref().map_or(true, |p| { + !a.named(&p.node.name) + }) }; // Generate the code for `form_request` parameters. @@ -170,7 +199,10 @@ fn generic_route_decorator(known_method: Option>, // 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); + debug!("Route params: {:?}", route); + let form_statement = route.generate_form_statement(ecx); + let query_statement = route.generate_query_statement(ecx); let param_statements = route.generate_param_statements(ecx); let fn_arguments = route.generate_fn_arguments(ecx); @@ -181,6 +213,7 @@ fn generic_route_decorator(known_method: Option>, fn $route_fn_name<'rocket>(_req: &'rocket ::rocket::Request<'rocket>) -> ::rocket::Response<'rocket> { $form_statement + $query_statement $param_statements let result = $user_fn_name($fn_arguments); ::rocket::Response::new(result) diff --git a/macros/src/parser/function.rs b/macros/src/parser/function.rs index 1ce5cbad..d57f763e 100644 --- a/macros/src/parser/function.rs +++ b/macros/src/parser/function.rs @@ -3,6 +3,7 @@ use syntax::codemap::{Span, Spanned}; use syntax::ext::base::Annotatable; use utils::{ArgExt, span}; +#[derive(Debug)] pub struct Function(Spanned<(Ident, FnDecl)>); impl Function { @@ -33,7 +34,7 @@ impl Function { self.0.span } - pub fn find_input<'a>(&'a self, name: &str) -> Option<&'a Arg> { + pub fn find_input<'a>(&'a self, name: &Name) -> Option<&'a Arg> { self.decl().inputs.iter().filter(|arg| arg.named(name)).next() } } diff --git a/macros/src/parser/param.rs b/macros/src/parser/param.rs index e2868628..fc8d61f1 100644 --- a/macros/src/parser/param.rs +++ b/macros/src/parser/param.rs @@ -1,5 +1,7 @@ +use syntax::ast::Ident; use syntax::ext::base::ExtCtxt; use syntax::codemap::{Span, Spanned, BytePos}; +use syntax::parse::token::str_to_ident; use utils::span; @@ -20,9 +22,9 @@ impl<'s, 'a, 'c: 'a> ParamIter<'s, 'a, 'c> { } impl<'s, 'a, 'c> Iterator for ParamIter<'s, 'a, 'c> { - type Item = Spanned<&'s str>; + type Item = Spanned; - fn next(&mut self) -> Option> { + 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), @@ -51,7 +53,7 @@ impl<'s, 'a, 'c> Iterator for ParamIter<'s, 'a, 'c> { } else { self.string = &self.string[(end + 1)..]; self.span.lo = self.span.lo + BytePos((end + 1) as u32); - Some(span(param, param_span)) + Some(span(str_to_ident(param), param_span)) } } } diff --git a/macros/src/parser/route.rs b/macros/src/parser/route.rs index bae1e4ea..5d116128 100644 --- a/macros/src/parser/route.rs +++ b/macros/src/parser/route.rs @@ -4,6 +4,7 @@ use std::collections::HashSet; use syntax::ast::*; use syntax::ext::base::{ExtCtxt, Annotatable}; use syntax::codemap::{Span, Spanned, dummy_spanned}; +use syntax::parse::token::str_to_ident; use utils::{span, MetaItemExt, SpanExt}; use super::{Function, ParamIter}; @@ -16,11 +17,13 @@ use rocket::{Method, ContentType}; /// the user supplied the information. This structure can only be obtained by /// calling the `RouteParams::from` function and passing in the entire decorator /// environment. +#[derive(Debug)] pub struct RouteParams { pub annotated_fn: Function, pub method: Spanned, pub path: Spanned, - pub form_param: Option>, + pub form_param: Option>, + pub query_param: Option>, pub format: Option>, pub rank: Option>, } @@ -66,8 +69,8 @@ impl RouteParams { ecx.span_fatal(sp, "malformed attribute"); } - // Parse the required path parameter. - let path = parse_path(ecx, &attr_params[0]); + // Parse the required path and optional query parameters. + let (path, query) = parse_path(ecx, &attr_params[0]); // Parse all of the optional parameters. let mut seen_keys = HashSet::new(); @@ -105,6 +108,7 @@ impl RouteParams { method: method, path: path, form_param: form, + query_param: query, format: format, rank: rank, annotated_fn: function, @@ -137,6 +141,22 @@ pub fn kv_from_nested(item: &NestedMetaItem) -> Option> { }) } +fn param_string_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option { + let string = s.node; + if string.starts_with('<') && string.ends_with('>') { + let param = &string[1..(string.len() - 1)]; + if param.chars().all(char::is_alphanumeric) { + return Some(str_to_ident(param)); + } + + ecx.span_err(s.span, "parameter name must be alphanumeric"); + } else { + ecx.span_err(s.span, "parameters must start with '<' and end with '>'"); + } + + None +} + 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()) { @@ -157,18 +177,29 @@ fn parse_method(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned { return dummy_spanned(Method::Get); } -fn parse_path(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned { +fn parse_path(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> (Spanned, Option>) { + let from_string = |string: &str, sp: Span| { + if let Some(q) = string.find('?') { + let path = span(string[..q].to_string(), sp); + let q_str = span(&string[(q + 1)..], sp); + let query = param_string_to_ident(ecx, q_str).map(|i| span(i, sp)); + return (path, query); + } else { + return (span(string.to_string(), sp), None) + } + }; + 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); + return from_string(s, 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); + return from_string(s, 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, \ @@ -177,7 +208,7 @@ fn parse_path(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned { .emit(); } - dummy_spanned("".to_string()) + (dummy_spanned("".to_string()), None) } fn parse_opt(ecx: &ExtCtxt, kv: &KVSpanned, f: F) -> Option> @@ -186,15 +217,10 @@ fn parse_opt(ecx: &ExtCtxt, kv: &KVSpanned, f: F) -> Option) -> String { +fn parse_form(ecx: &ExtCtxt, kv: &KVSpanned) -> Ident { 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"); + if let Some(ident) = param_string_to_ident(ecx, span(s, kv.value.span)) { + return ident; } } @@ -204,7 +230,7 @@ fn parse_form(ecx: &ExtCtxt, kv: &KVSpanned) -> String { parameter inside '<' '>'. e.g: form = """#) .emit(); - "".to_string() + str_to_ident("") } fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned) -> isize { diff --git a/macros/src/utils/arg_ext.rs b/macros/src/utils/arg_ext.rs index 5a460e00..d3277a45 100644 --- a/macros/src/utils/arg_ext.rs +++ b/macros/src/utils/arg_ext.rs @@ -1,15 +1,15 @@ -use syntax::ast::{Arg, PatKind, Ident}; +use syntax::ast::{Arg, PatKind, Ident, Name}; pub trait ArgExt { fn ident(&self) -> Option<&Ident>; - fn name(&self) -> Option { + fn name(&self) -> Option<&Name> { self.ident().map(|ident| { - ident.name.to_string() + &ident.name }) } - fn named(&self, name: &str) -> bool { + fn named(&self, name: &Name) -> bool { self.name().map_or(false, |a| a == name) } }