Add query params to Rocket. Use Ident for attribute params.

This commit is contained in:
Sergio Benitez 2016-09-04 19:18:08 -07:00
parent ec38d70449
commit 327b28a98e
11 changed files with 170 additions and 97 deletions

View File

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

View File

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

View File

@ -1,5 +1,5 @@
[package]
name = "hello_query"
name = "query_params"
version = "0.0.1"
authors = ["Sergio Benitez <sb@sergio.bz>"]
workspace = "../../"

View File

@ -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<u8>
}
#[get("/hello?<person>")]
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]);
}

View File

@ -13,9 +13,9 @@ pub trait FromFormValue<'v>: Sized {
fn parse(v: &'v str) -> Result<Self, Self::Error>;
// 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<Self> {
None
}
@ -95,27 +95,37 @@ impl<'v, T: FromFormValue<'v>> FromFormValue<'v> for Result<T, T::Error> {
}
}
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<Self::Item> {
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))
}
}
param_num
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_count
}
#[cfg(test)]

View File

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

View File

@ -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<ContentType>) -> Option<P<Expr
}
trait RouteGenerateExt {
fn gen_form(&self, &ExtCtxt, Option<&Spanned<Ident>>, P<Expr>) -> Option<Stmt>;
fn missing_declared_err<T: Display>(&self, ecx: &ExtCtxt, arg: &Spanned<T>);
fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option<Stmt>;
fn generate_query_statement(&self, ecx: &ExtCtxt) -> Option<Stmt>;
fn generate_param_statements(&self, ecx: &ExtCtxt) -> Vec<Stmt>;
fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec<TokenTree>;
fn explode(&self, ecx: &ExtCtxt) -> (&String, Path, P<Expr>, P<Expr>);
}
impl RouteGenerateExt for RouteParams {
fn generate_form_statement(&self, ecx: &ExtCtxt) -> Option<Stmt> {
let param = self.form_param.as_ref();
let arg = param.and_then(|p| self.annotated_fn.find_input(p.value()));
fn missing_declared_err<T: Display>(&self, ecx: &ExtCtxt, arg: &Spanned<T>) {
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<Ident>>,
form_string: P<Expr>) -> Option<Stmt> {
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, &param.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<Stmt> {
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<Stmt> {
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<Stmt> {
@ -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 &params {
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(&param.node.name).is_none() {
self.missing_declared_err(ecx, &param);
}
}
// 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<Spanned<Method>>,
// 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<Spanned<Method>>,
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)

View File

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

View File

@ -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<Ident>;
fn next(&mut self) -> Option<Spanned<&'s str>> {
fn next(&mut self) -> Option<Spanned<Ident>> {
// 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))
}
}
}

View File

@ -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<Method>,
pub path: Spanned<String>,
pub form_param: Option<KVSpanned<String>>,
pub form_param: Option<KVSpanned<Ident>>,
pub query_param: Option<Spanned<Ident>>,
pub format: Option<KVSpanned<ContentType>>,
pub rank: Option<KVSpanned<isize>>,
}
@ -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<KVSpanned<LitKind>> {
})
}
fn param_string_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option<Ident> {
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<Method> {
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<Method> {
return dummy_spanned(Method::Get);
}
fn parse_path(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> Spanned<String> {
fn parse_path(ecx: &ExtCtxt, meta_item: &NestedMetaItem) -> (Spanned<String>, Option<Spanned<Ident>>) {
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<String> {
.emit();
}
dummy_spanned("".to_string())
(dummy_spanned("".to_string()), None)
}
fn parse_opt<O, T, F>(ecx: &ExtCtxt, kv: &KVSpanned<T>, f: F) -> Option<KVSpanned<O>>
@ -186,15 +217,10 @@ fn parse_opt<O, T, F>(ecx: &ExtCtxt, kv: &KVSpanned<T>, f: F) -> Option<KVSpanne
Some(kv.map_ref(|_| f(ecx, kv)))
}
fn parse_form(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> String {
fn parse_form(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> 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<LitKind>) -> String {
parameter inside '<' '>'. e.g: form = "<login>""#)
.emit();
"".to_string()
str_to_ident("")
}
fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> isize {

View File

@ -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<String> {
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)
}
}