mirror of https://github.com/rwf2/Rocket.git
Reimplement route attribute as a proc-macro.
This commits also implement the query reform from #608. It also consists of many, many breaking changes. Among them are: * Query parts in route paths use new query reform syntax. * Routing for queries is now lenient. - Default ranking has changed to reflect query reform. * Format routing matching has been fixed. - Routes with formats matching "accept" will always collide. - Routes with formats matching "content-type" require requests to have an equivalent content-type header to match. - Requests with imprecise content-types are treated as not having a content-type. * Generated routes and catchers respect visibility modifiers. * Raw getter methods from request were renamed and retooled. - In particular, the index parameter is based on segments in the route path, not dynamic parameters. * The method-based attributes no longer accept a keyed 'path'. * The 'rocket_codegen' crate is gone and will no longer be public. * The 'FormItems' iterator emits values of type 'FormItem'. - The internal form items' string can no longer be retrieved. * In general, routes are more strictly validated. * Logging from codegen now funnels through logging infrastructure. * Routing has been optimized by caching routing metadata. Resolves #93. Resolves #608. Resolves #693. Resolves #476.
This commit is contained in:
parent
3eb873d89d
commit
61f107f550
|
@ -10,8 +10,7 @@ Rocket is web framework for Rust (nightly) with a focus on ease-of-use,
|
|||
expressibility, and speed. Here's an example of a complete Rocket application:
|
||||
|
||||
```rust
|
||||
#![feature(plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -96,15 +96,15 @@ pub fn database_attr(attr: TokenStream, input: TokenStream) -> Result<TokenStrea
|
|||
match pool {
|
||||
Ok(Ok(p)) => Ok(rocket.manage(#pool_type(p))),
|
||||
Err(config_error) => {
|
||||
::rocket::logger::log_err(false,
|
||||
::rocket::logger::log_error(
|
||||
&format!("Database configuration failure: '{}'", #name));
|
||||
::rocket::logger::log_err(true, &format!("{}", config_error));
|
||||
::rocket::logger::log_error_(&format!("{}", config_error));
|
||||
Err(rocket)
|
||||
},
|
||||
Ok(Err(pool_error)) => {
|
||||
::rocket::logger::log_err(false,
|
||||
::rocket::logger::log_error(
|
||||
&format!("Failed to initialize pool for '{}'", #name));
|
||||
::rocket::logger::log_err(true, &format!("{:?}", pool_error));
|
||||
::rocket::logger::log_error_(&format!("{:?}", pool_error));
|
||||
Err(rocket)
|
||||
},
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@
|
|||
//! This crate is expected to grow with time, bringing in outside crates to be
|
||||
//! officially supported by Rocket.
|
||||
|
||||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate rocket;
|
||||
#[allow(unused_imports)] #[macro_use] extern crate log;
|
||||
#[allow(unused_imports)] #[macro_use] extern crate rocket;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
extern crate serde;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
mod route;
|
||||
|
||||
pub use self::route::*;
|
|
@ -1,395 +0,0 @@
|
|||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
|
||||
use ::{ROUTE_STRUCT_PREFIX, ROUTE_FN_PREFIX, PARAM_PREFIX, URI_INFO_MACRO_PREFIX};
|
||||
use ::{ROUTE_ATTR, ROUTE_INFO_ATTR};
|
||||
use parser::{Param, RouteParams};
|
||||
use utils::*;
|
||||
|
||||
use syntax::source_map::{Span, Spanned, dummy_spanned};
|
||||
use syntax::tokenstream::TokenTree;
|
||||
use syntax::ast::{Arg, Ident, Item, Stmt, Expr, MetaItem, Path};
|
||||
use syntax::ext::base::{Annotatable, ExtCtxt};
|
||||
use syntax::ext::build::AstBuilder;
|
||||
use syntax::parse::token;
|
||||
use syntax::symbol::LocalInternedString;
|
||||
use syntax::ptr::P;
|
||||
|
||||
use rocket_http::{Method, MediaType};
|
||||
|
||||
fn method_to_path(ecx: &ExtCtxt, method: Method) -> Path {
|
||||
quote_enum!(ecx, method => ::rocket_http::Method -> ::rocket::http::Method {
|
||||
Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch;
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME: I think we also want the attributes here.
|
||||
fn media_type_to_expr(ecx: &ExtCtxt, ct: Option<MediaType>) -> Option<P<Expr>> {
|
||||
ct.map(|ct| {
|
||||
let (top, sub) = (ct.top().as_str(), ct.sub().as_str());
|
||||
quote_expr!(ecx, ::rocket::http::MediaType {
|
||||
source: ::rocket::http::Source::None,
|
||||
top: ::rocket::http::Indexed::Concrete(
|
||||
::std::borrow::Cow::Borrowed($top)
|
||||
),
|
||||
sub: ::rocket::http::Indexed::Concrete(
|
||||
::std::borrow::Cow::Borrowed($sub)
|
||||
),
|
||||
params: ::rocket::http::MediaParams::Static(&[])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
impl RouteParams {
|
||||
fn missing_declared_err<T: Display>(&self, ecx: &ExtCtxt, arg: &Spanned<T>) {
|
||||
let (fn_span, fn_name) = (self.annotated_fn.span(), self.annotated_fn.ident());
|
||||
ecx.struct_span_err(arg.span, &format!("unused dynamic parameter: `{}`", arg.node))
|
||||
.span_note(fn_span, &format!("expected argument named `{}` in `{}` handler",
|
||||
arg.node, fn_name))
|
||||
.emit();
|
||||
}
|
||||
|
||||
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() {
|
||||
self.missing_declared_err(ecx, param.unwrap());
|
||||
return None;
|
||||
}
|
||||
|
||||
let arg = arg.unwrap();
|
||||
let name = arg.ident().expect("form param identifier").prepend(PARAM_PREFIX);
|
||||
let ty = strip_ty_lifetimes(arg.ty.clone());
|
||||
Some(quote_stmt!(ecx,
|
||||
#[allow(non_snake_case)]
|
||||
let $name: $ty = {
|
||||
let mut items = ::rocket::request::FormItems::from($form_string);
|
||||
let form = ::rocket::request::FromForm::from_form(items.by_ref(), true);
|
||||
#[allow(unreachable_patterns)]
|
||||
let obj = match form {
|
||||
Ok(v) => v,
|
||||
Err(_) => return ::rocket::Outcome::Forward(__data)
|
||||
};
|
||||
|
||||
if !items.exhaust() {
|
||||
println!(" => The query string {:?} is malformed.", $form_string);
|
||||
return ::rocket::Outcome::Failure(::rocket::http::Status::BadRequest);
|
||||
}
|
||||
|
||||
obj
|
||||
}
|
||||
).expect("form statement"))
|
||||
}
|
||||
|
||||
fn generate_data_statements(&self, ecx: &ExtCtxt) -> Option<(Stmt, Stmt)> {
|
||||
let param = self.data_param.as_ref().map(|p| &p.value)?;
|
||||
let arg = self.annotated_fn.find_input(¶m.node.name);
|
||||
if arg.is_none() {
|
||||
self.missing_declared_err(ecx, param);
|
||||
return None;
|
||||
}
|
||||
|
||||
let arg = arg.unwrap();
|
||||
let name = arg.ident().expect("form param identifier").prepend(PARAM_PREFIX);
|
||||
let ty = strip_ty_lifetimes(arg.ty.clone());
|
||||
|
||||
let transform_stmt = quote_stmt!(ecx,
|
||||
let __transform = <$ty as ::rocket::data::FromData>::transform(__req, __data);
|
||||
).expect("data statement");
|
||||
|
||||
let data_stmt = quote_stmt!(ecx,
|
||||
#[allow(non_snake_case, unreachable_patterns)]
|
||||
let $name: $ty = {
|
||||
let __outcome = match __transform {
|
||||
::rocket::data::Transform::Owned(::rocket::Outcome::Success(__v)) => {
|
||||
::rocket::data::Transform::Owned(::rocket::Outcome::Success(__v))
|
||||
}
|
||||
::rocket::data::Transform::Borrowed(::rocket::Outcome::Success(ref v)) => {
|
||||
let borrow = ::std::borrow::Borrow::borrow(v);
|
||||
::rocket::data::Transform::Borrowed(::rocket::Outcome::Success(borrow))
|
||||
}
|
||||
::rocket::data::Transform::Owned(inner) => {
|
||||
::rocket::data::Transform::Owned(inner)
|
||||
}
|
||||
::rocket::data::Transform::Borrowed(inner) => {
|
||||
::rocket::data::Transform::Borrowed(inner.map(|_| unreachable!()))
|
||||
}
|
||||
};
|
||||
|
||||
match <$ty as ::rocket::data::FromData>::from_data(__req, __outcome) {
|
||||
::rocket::Outcome::Success(d) => d,
|
||||
::rocket::Outcome::Forward(d) => {
|
||||
return ::rocket::Outcome::Forward(d);
|
||||
}
|
||||
::rocket::Outcome::Failure((code, _)) => {
|
||||
return ::rocket::Outcome::Failure(code);
|
||||
}
|
||||
}
|
||||
};
|
||||
).expect("data statement");
|
||||
|
||||
Some((transform_stmt, data_stmt))
|
||||
}
|
||||
|
||||
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() {
|
||||
Some(query) => query,
|
||||
None => return ::rocket::Outcome::Forward(__data)
|
||||
}
|
||||
);
|
||||
|
||||
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> {
|
||||
let mut fn_param_statements = vec![];
|
||||
let params = Param::parse_many(ecx, self.uri.node.path(), self.uri.span.trim(1))
|
||||
.unwrap_or_else(|mut diag| { diag.emit(); vec![] });
|
||||
|
||||
// Generate a statement for every declared paramter in the path.
|
||||
let mut declared_set = HashSet::new();
|
||||
for (i, param) in params.iter().enumerate() {
|
||||
declared_set.insert(param.ident().name);
|
||||
let ty = match self.annotated_fn.find_input(¶m.ident().name) {
|
||||
Some(arg) => strip_ty_lifetimes(arg.ty.clone()),
|
||||
None => {
|
||||
self.missing_declared_err(ecx, param.inner());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Note: the `None` case shouldn't happen if a route is matched.
|
||||
let ident = param.ident().prepend(PARAM_PREFIX);
|
||||
let expr = match *param {
|
||||
Param::Single(_) => quote_expr!(ecx, match __req.get_param_str($i) {
|
||||
Some(s) => <$ty as ::rocket::request::FromParam>::from_param(s),
|
||||
None => return ::rocket::Outcome::Forward(__data)
|
||||
}),
|
||||
Param::Many(_) => quote_expr!(ecx, match __req.get_raw_segments($i) {
|
||||
Some(s) => <$ty as ::rocket::request::FromSegments>::from_segments(s),
|
||||
None => return ::rocket::Outcome::Forward(__data)
|
||||
}),
|
||||
};
|
||||
|
||||
let original_ident = param.ident();
|
||||
fn_param_statements.push(quote_stmt!(ecx,
|
||||
#[allow(non_snake_case, unreachable_patterns)]
|
||||
let $ident: $ty = match $expr {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
println!(" => Failed to parse '{}': {:?}",
|
||||
stringify!($original_ident), e);
|
||||
return ::rocket::Outcome::Forward(__data)
|
||||
}
|
||||
};
|
||||
).expect("declared param parsing statement"));
|
||||
}
|
||||
|
||||
// A from_request parameter is one that isn't declared, data, or query.
|
||||
let from_request = |a: &&Arg| {
|
||||
if let Some(name) = a.name() {
|
||||
!declared_set.contains(name)
|
||||
&& self.data_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)
|
||||
})
|
||||
} else {
|
||||
ecx.span_err(a.pat.span, "route argument names must be identifiers");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// Generate the code for `from_request` parameters.
|
||||
let all = &self.annotated_fn.decl().inputs;
|
||||
for arg in all.iter().filter(from_request) {
|
||||
let ident = arg.ident().unwrap().prepend(PARAM_PREFIX);
|
||||
let ty = strip_ty_lifetimes(arg.ty.clone());
|
||||
fn_param_statements.push(quote_stmt!(ecx,
|
||||
#[allow(non_snake_case, unreachable_patterns)]
|
||||
let $ident: $ty = match
|
||||
::rocket::request::FromRequest::from_request(__req) {
|
||||
::rocket::Outcome::Success(v) => v,
|
||||
::rocket::Outcome::Forward(_) =>
|
||||
return ::rocket::Outcome::Forward(__data),
|
||||
::rocket::Outcome::Failure((code, _)) => {
|
||||
return ::rocket::Outcome::Failure(code)
|
||||
},
|
||||
};
|
||||
).expect("undeclared param parsing statement"));
|
||||
}
|
||||
|
||||
fn_param_statements
|
||||
}
|
||||
|
||||
fn generate_fn_arguments(&self, ecx: &ExtCtxt) -> Vec<TokenTree> {
|
||||
let args = self.annotated_fn.decl().inputs.iter()
|
||||
.filter_map(|a| a.ident())
|
||||
.map(|ident| ident.prepend(PARAM_PREFIX))
|
||||
.collect::<Vec<Ident>>();
|
||||
|
||||
sep_by_tok(ecx, &args, token::Comma)
|
||||
}
|
||||
|
||||
fn generate_uri_macro(&self, ecx: &ExtCtxt) -> P<Item> {
|
||||
let macro_args = parse_as_tokens(ecx, "$($token:tt)*");
|
||||
let macro_exp = parse_as_tokens(ecx, "$($token)*");
|
||||
let macro_name = self.annotated_fn.ident().prepend(URI_INFO_MACRO_PREFIX);
|
||||
|
||||
// What we return if we find an inconsistency throughout.
|
||||
let dummy = quote_item!(ecx, pub macro $macro_name($macro_args) { }).unwrap();
|
||||
|
||||
// Hacky check to see if the user's URI was valid.
|
||||
if self.uri.span == dummy_spanned(()).span {
|
||||
return dummy
|
||||
}
|
||||
|
||||
// Extract the route uri path and paramters from the uri.
|
||||
let route_path = self.uri.node.to_string();
|
||||
let params = match Param::parse_many(ecx, &route_path, self.uri.span.trim(1)) {
|
||||
Ok(params) => params,
|
||||
Err(mut diag) => {
|
||||
diag.cancel();
|
||||
return dummy;
|
||||
}
|
||||
};
|
||||
|
||||
// Generate the list of arguments for the URI.
|
||||
let mut fn_uri_args = vec![];
|
||||
for param in ¶ms {
|
||||
if let Some(arg) = self.annotated_fn.find_input(¶m.ident().name) {
|
||||
let (pat, ty) = (&arg.pat, &arg.ty);
|
||||
fn_uri_args.push(quote_tokens!(ecx, $pat: $ty))
|
||||
} else {
|
||||
return dummy;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the call to the internal URI macro with all the info.
|
||||
let args = sep_by_tok(ecx, &fn_uri_args, token::Comma);
|
||||
quote_item!(ecx,
|
||||
pub macro $macro_name($macro_args) {
|
||||
rocket_internal_uri!($route_path, ($args), $macro_exp)
|
||||
}
|
||||
).expect("consistent uri macro item")
|
||||
}
|
||||
|
||||
fn explode(&self, ecx: &ExtCtxt) -> (LocalInternedString, String, Path, P<Expr>, P<Expr>) {
|
||||
let name = self.annotated_fn.ident().name.as_str();
|
||||
let path = self.uri.node.to_string();
|
||||
let method = method_to_path(ecx, self.method.node);
|
||||
let format = self.format.as_ref().map(|kv| kv.value().clone());
|
||||
let media_type = option_as_expr(ecx, &media_type_to_expr(ecx, format));
|
||||
let rank = option_as_expr(ecx, &self.rank);
|
||||
|
||||
(name, path, method, media_type, rank)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Compilation fails when parameters have the same name as the function!
|
||||
fn generic_route_decorator(known_method: Option<Spanned<Method>>,
|
||||
ecx: &mut ExtCtxt,
|
||||
sp: Span,
|
||||
meta_item: &MetaItem,
|
||||
annotated: Annotatable
|
||||
) -> Vec<Annotatable> {
|
||||
let mut output = Vec::new();
|
||||
|
||||
// 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 param_statements = route.generate_param_statements(ecx);
|
||||
let query_statement = route.generate_query_statement(ecx);
|
||||
let fn_arguments = route.generate_fn_arguments(ecx);
|
||||
let uri_macro = route.generate_uri_macro(ecx);
|
||||
let (transform_statement, data_statement) = route.generate_data_statements(ecx)
|
||||
.map(|(a, b)| (Some(a), Some(b)))
|
||||
.unwrap_or((None, None));
|
||||
|
||||
// Generate and emit the wrapping function with the Rocket handler signature.
|
||||
let user_fn_name = route.annotated_fn.ident();
|
||||
let route_fn_name = user_fn_name.prepend(ROUTE_FN_PREFIX);
|
||||
emit_item(&mut output, quote_item!(ecx,
|
||||
// Allow the `unreachable_code` lint for those FromParam impls that have
|
||||
// an `Error` associated type of !.
|
||||
#[allow(unreachable_code)]
|
||||
fn $route_fn_name<'_b>(__req: &'_b ::rocket::Request, __data: ::rocket::Data)
|
||||
-> ::rocket::handler::Outcome<'_b> {
|
||||
$param_statements
|
||||
$query_statement
|
||||
$transform_statement
|
||||
$data_statement
|
||||
let responder = $user_fn_name($fn_arguments);
|
||||
::rocket::handler::Outcome::from(__req, responder)
|
||||
}
|
||||
).unwrap());
|
||||
|
||||
// Generate and emit the static route info that uses the just generated
|
||||
// function as its handler. A proper Rocket route will be created from this.
|
||||
let struct_name = user_fn_name.prepend(ROUTE_STRUCT_PREFIX);
|
||||
let (name, path, method, media_type, rank) = route.explode(ecx);
|
||||
let static_route_info_item = quote_item!(ecx,
|
||||
/// Rocket code generated static route information structure.
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub static $struct_name: ::rocket::StaticRouteInfo =
|
||||
::rocket::StaticRouteInfo {
|
||||
name: $name,
|
||||
method: $method,
|
||||
path: $path,
|
||||
handler: $route_fn_name,
|
||||
format: $media_type,
|
||||
rank: $rank,
|
||||
};
|
||||
).expect("static route info");
|
||||
|
||||
// Attach a `rocket_route_info` attribute to the route info and emit it.
|
||||
let attr_name = Ident::from_str(ROUTE_INFO_ATTR);
|
||||
let info_attr = quote_attr!(ecx, #[$attr_name]);
|
||||
attach_and_emit(&mut output, info_attr, Annotatable::Item(static_route_info_item));
|
||||
|
||||
// Attach a `rocket_route` attribute to the user's function and emit it.
|
||||
let attr_name = Ident::from_str(ROUTE_ATTR);
|
||||
let route_attr = quote_attr!(ecx, #[$attr_name($struct_name)]);
|
||||
attach_and_emit(&mut output, route_attr, annotated);
|
||||
|
||||
// Emit the per-route URI macro.
|
||||
emit_item(&mut output, uri_macro);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn route_decorator(
|
||||
ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, annotated: Annotatable
|
||||
) -> Vec<Annotatable> {
|
||||
generic_route_decorator(None, ecx, sp, meta_item, annotated)
|
||||
}
|
||||
|
||||
macro_rules! method_decorator {
|
||||
($name:ident, $method:ident) => (
|
||||
pub fn $name(
|
||||
ecx: &mut ExtCtxt, sp: Span, meta_item: &MetaItem, annotated: Annotatable
|
||||
) -> Vec<Annotatable> {
|
||||
let i_sp = meta_item.span.shorten_to(stringify!($method).len());
|
||||
let method = Some(span(Method::$method, i_sp));
|
||||
generic_route_decorator(method, ecx, sp, meta_item, annotated)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
method_decorator!(get_decorator, Get);
|
||||
method_decorator!(put_decorator, Put);
|
||||
method_decorator!(post_decorator, Post);
|
||||
method_decorator!(delete_decorator, Delete);
|
||||
method_decorator!(head_decorator, Head);
|
||||
method_decorator!(patch_decorator, Patch);
|
||||
method_decorator!(options_decorator, Options);
|
|
@ -1,8 +1,4 @@
|
|||
#![crate_type = "dylib"]
|
||||
#![feature(quote, concat_idents, plugin_registrar, rustc_private)]
|
||||
#![feature(custom_attribute)]
|
||||
#![allow(unused_attributes)]
|
||||
#![allow(deprecated)]
|
||||
|
||||
// TODO: Version URLs.
|
||||
#![doc(html_root_url = "https://api.rocket.rs")]
|
||||
|
@ -389,51 +385,3 @@
|
|||
//! ```
|
||||
//! ROCKET_CODEGEN_DEBUG=1 cargo build
|
||||
//! ```
|
||||
|
||||
extern crate syntax;
|
||||
extern crate syntax_ext;
|
||||
extern crate syntax_pos;
|
||||
extern crate rustc_plugin;
|
||||
extern crate rocket_http;
|
||||
extern crate indexmap;
|
||||
|
||||
#[macro_use] mod utils;
|
||||
mod parser;
|
||||
mod decorators;
|
||||
|
||||
use rustc_plugin::Registry;
|
||||
use syntax::ext::base::SyntaxExtension;
|
||||
use syntax::symbol::Symbol;
|
||||
|
||||
const DEBUG_ENV_VAR: &str = "ROCKET_CODEGEN_DEBUG";
|
||||
|
||||
const PARAM_PREFIX: &str = "rocket_param_";
|
||||
const ROUTE_STRUCT_PREFIX: &str = "static_rocket_route_info_for_";
|
||||
const ROUTE_FN_PREFIX: &str = "rocket_route_fn_";
|
||||
const URI_INFO_MACRO_PREFIX: &str = "rocket_uri_for_";
|
||||
|
||||
const ROUTE_ATTR: &str = "rocket_route";
|
||||
const ROUTE_INFO_ATTR: &str = "rocket_route_info";
|
||||
|
||||
macro_rules! register_decorators {
|
||||
($registry:expr, $($name:expr => $func:ident),+) => (
|
||||
$($registry.register_syntax_extension(Symbol::intern($name),
|
||||
SyntaxExtension::MultiModifier(Box::new(decorators::$func)));
|
||||
)+
|
||||
)
|
||||
}
|
||||
|
||||
/// Compiler hook for Rust to register plugins.
|
||||
#[plugin_registrar]
|
||||
pub fn plugin_registrar(reg: &mut Registry) {
|
||||
register_decorators!(reg,
|
||||
"route" => route_decorator,
|
||||
"get" => get_decorator,
|
||||
"put" => put_decorator,
|
||||
"post" => post_decorator,
|
||||
"delete" => delete_decorator,
|
||||
"head" => head_decorator,
|
||||
"patch" => patch_decorator,
|
||||
"options" => options_decorator
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
use syntax::ast::*;
|
||||
use syntax::source_map::{Span, Spanned};
|
||||
use syntax::ext::base::Annotatable;
|
||||
use utils::{ArgExt, span};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Function(Spanned<(Ident, FnDecl)>);
|
||||
|
||||
impl Function {
|
||||
pub fn from(annotated: &Annotatable) -> Result<Function, Span> {
|
||||
if let Annotatable::Item(ref item) = *annotated {
|
||||
if let ItemKind::Fn(ref decl, ..) = item.node {
|
||||
let inner = (item.ident, decl.clone().into_inner());
|
||||
return Ok(Function(span(inner, item.span)));
|
||||
}
|
||||
}
|
||||
|
||||
Err(annotated.span())
|
||||
}
|
||||
|
||||
pub fn ident(&self) -> &Ident {
|
||||
&self.0.node.0
|
||||
}
|
||||
|
||||
pub fn decl(&self) -> &FnDecl {
|
||||
&self.0.node.1
|
||||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
self.0.span
|
||||
}
|
||||
|
||||
pub fn find_input<'a>(&'a self, name: &Name) -> Option<&'a Arg> {
|
||||
self.decl().inputs.iter().find(|arg| arg.named(name))
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
use syntax::source_map::{Spanned, Span, dummy_spanned, DUMMY_SP};
|
||||
use syntax::ext::base::ExtCtxt;
|
||||
use syntax::tokenstream::TokenTree;
|
||||
use syntax::ext::quote::rt::ToTokens;
|
||||
use utils::span;
|
||||
|
||||
/// A spanned key-value pair in an attribute.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KVSpanned<V> {
|
||||
/// Span for the full key/value pair.
|
||||
pub span: Span,
|
||||
/// The spanned key.
|
||||
pub key: Spanned<String>,
|
||||
/// The spanned value.
|
||||
pub value: Spanned<V>
|
||||
}
|
||||
|
||||
impl<V> KVSpanned<V> {
|
||||
/// Maps a reference of the inner value of this span using `f`. The key,
|
||||
/// value, and full spans will remain the same in the new KVSpan.
|
||||
pub fn map_ref<U, F: FnOnce(&V) -> U>(&self, f: F) -> KVSpanned<U> {
|
||||
KVSpanned {
|
||||
span: self.span,
|
||||
key: self.key.clone(),
|
||||
value: span(f(&self.value.node), self.value.span)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the unspanned key. Purely for convenience.
|
||||
pub fn key(&self) -> &String {
|
||||
&self.key.node
|
||||
}
|
||||
|
||||
/// Returns the unspanned value. Purely for convenience.
|
||||
pub fn value(&self) -> &V {
|
||||
&self.value.node
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default> Default for KVSpanned<T> {
|
||||
/// Returns a dummy KVSpan with an empty key for the default value of T.
|
||||
fn default() -> KVSpanned<T> {
|
||||
dummy_kvspanned("", T::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToTokens> ToTokens for KVSpanned<T> {
|
||||
fn to_tokens(&self, cx: &ExtCtxt) -> Vec<TokenTree> {
|
||||
self.value().to_tokens(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a KVSpanned value with dummy (meaningless) spans.
|
||||
pub fn dummy_kvspanned<T, S: ToString>(key: S, value: T) -> KVSpanned<T> {
|
||||
KVSpanned {
|
||||
span: DUMMY_SP,
|
||||
key: dummy_spanned(key.to_string()),
|
||||
value: dummy_spanned(value),
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
mod keyvalue;
|
||||
mod route;
|
||||
mod param;
|
||||
mod function;
|
||||
mod uri;
|
||||
|
||||
pub use self::keyvalue::KVSpanned;
|
||||
pub use self::route::RouteParams;
|
||||
pub use self::param::Param;
|
||||
pub use self::function::Function;
|
|
@ -1,66 +0,0 @@
|
|||
use syntax::ast::Ident;
|
||||
use syntax::source_map::{Span, Spanned};
|
||||
use syntax::ext::base::ExtCtxt;
|
||||
use utils::SpanExt;
|
||||
use syntax::parse::PResult;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Param {
|
||||
Single(Spanned<Ident>),
|
||||
Many(Spanned<Ident>),
|
||||
}
|
||||
|
||||
impl Param {
|
||||
pub fn inner(&self) -> &Spanned<Ident> {
|
||||
match *self {
|
||||
Param::Single(ref ident) | Param::Many(ref ident) => ident,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ident(&self) -> &Ident {
|
||||
match *self {
|
||||
Param::Single(ref ident) | Param::Many(ref ident) => &ident.node,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_many<'a>(
|
||||
ecx: &ExtCtxt<'a>,
|
||||
mut string: &str,
|
||||
mut span: Span
|
||||
) -> PResult<'a, Vec<Param>> {
|
||||
let err = |sp, msg| { Err(ecx.struct_span_err(sp, msg)) };
|
||||
|
||||
let mut params = vec![];
|
||||
loop {
|
||||
// Find the start and end indexes for the next parameter, if any.
|
||||
let (start, end) = match string.find('<') {
|
||||
Some(i) => match string.find('>') {
|
||||
Some(j) if j > i => (i, j),
|
||||
Some(_) => return err(span, "malformed parameters"),
|
||||
None => return err(span, "malformed parameters")
|
||||
},
|
||||
_ => return Ok(params)
|
||||
};
|
||||
|
||||
// Calculate the parameter's ident and span.
|
||||
let param_span = span.trim_left(start).shorten_to(end + 1);
|
||||
let full_param = &string[(start + 1)..end];
|
||||
let (is_many, param) = if full_param.ends_with("..") {
|
||||
(true, &full_param[..(full_param.len() - 2)])
|
||||
} else {
|
||||
(false, full_param)
|
||||
};
|
||||
|
||||
// Advance the string and span.
|
||||
string = &string[(end + 1)..];
|
||||
span = span.trim_left(end + 1);
|
||||
|
||||
let spanned_ident = param_span.wrap(Ident::from_str(param));
|
||||
if is_many {
|
||||
params.push(Param::Many(spanned_ident))
|
||||
} else {
|
||||
params.push(Param::Single(spanned_ident))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,281 +0,0 @@
|
|||
use std::str::FromStr;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use syntax::ast::*;
|
||||
use syntax::ext::base::{ExtCtxt, Annotatable};
|
||||
use syntax::source_map::{Span, Spanned, dummy_spanned};
|
||||
|
||||
use utils::{MetaItemExt, SpanExt, span, is_valid_ident};
|
||||
use super::Function;
|
||||
use super::keyvalue::KVSpanned;
|
||||
use super::uri::validate_uri;
|
||||
use rocket_http::{Method, MediaType};
|
||||
use rocket_http::uri::Origin;
|
||||
|
||||
/// This structure represents the parsed `route` attribute.
|
||||
///
|
||||
/// It contains all of the information supplied by the user and the span where
|
||||
/// 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 uri: Spanned<Origin<'static>>,
|
||||
pub data_param: Option<KVSpanned<Ident>>,
|
||||
pub query_param: Option<Spanned<Ident>>,
|
||||
pub format: Option<KVSpanned<MediaType>>,
|
||||
pub rank: Option<KVSpanned<isize>>,
|
||||
}
|
||||
|
||||
impl RouteParams {
|
||||
/// Parses the route attribute from the given decorator context. If the
|
||||
/// parse is not successful, this function exits early with the appropriate
|
||||
/// error message to the user.
|
||||
pub fn from(
|
||||
ecx: &mut ExtCtxt,
|
||||
sp: Span,
|
||||
known_method: Option<Spanned<Method>>,
|
||||
meta_item: &MetaItem,
|
||||
annotated: &Annotatable
|
||||
) -> RouteParams {
|
||||
let function = Function::from(annotated).unwrap_or_else(|item_sp| {
|
||||
ecx.span_err(sp, "this attribute can only be used on functions...");
|
||||
ecx.span_fatal(item_sp, "...but was applied to the item below.");
|
||||
});
|
||||
|
||||
let meta_items = meta_item.meta_item_list().unwrap_or_else(|| {
|
||||
ecx.struct_span_err(sp, "incorrect use of attribute")
|
||||
.help("attributes in Rocket must have the form: #[name(...)]")
|
||||
.emit();
|
||||
ecx.span_fatal(sp, "malformed attribute");
|
||||
});
|
||||
|
||||
if meta_items.is_empty() {
|
||||
ecx.span_fatal(sp, "attribute requires at least 1 parameter");
|
||||
}
|
||||
|
||||
// Figure out the method. If it is known (i.e, because we're parsing a
|
||||
// helper attribute), use that method directly. Otherwise, try to parse
|
||||
// it from the list of meta items.
|
||||
let (method, attr_params) = match known_method {
|
||||
Some(method) => (method, meta_items),
|
||||
None => (parse_method(ecx, &meta_items[0]), &meta_items[1..])
|
||||
};
|
||||
|
||||
if attr_params.is_empty() {
|
||||
ecx.struct_span_err(sp, "attribute requires at least a path")
|
||||
.help(r#"example: #[get("/my/path")] or #[get(path = "/hi")]"#)
|
||||
.emit();
|
||||
ecx.span_fatal(sp, "malformed attribute");
|
||||
}
|
||||
|
||||
// Parse the required path and optional query parameters.
|
||||
let (uri, query) = parse_path(ecx, &attr_params[0]);
|
||||
|
||||
// Parse all of the optional parameters.
|
||||
let mut seen_keys = HashSet::new();
|
||||
let (mut rank, mut data, mut format) = Default::default();
|
||||
for param in &attr_params[1..] {
|
||||
let kv_opt = kv_from_nested(param);
|
||||
if kv_opt.is_none() {
|
||||
ecx.span_err(param.span(), "expected key = value");
|
||||
continue;
|
||||
}
|
||||
|
||||
let kv = kv_opt.unwrap();
|
||||
match kv.key().as_str() {
|
||||
"rank" => rank = parse_opt(ecx, &kv, parse_rank),
|
||||
"data" => data = parse_opt(ecx, &kv, parse_data),
|
||||
"format" => format = parse_opt(ecx, &kv, parse_format),
|
||||
_ => {
|
||||
let msg = format!("'{}' is not a known parameter", kv.key());
|
||||
ecx.span_err(kv.span, &msg);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if seen_keys.contains(kv.key()) {
|
||||
let msg = format!("{} was already defined", kv.key());
|
||||
ecx.struct_span_warn(param.span, &msg)
|
||||
.note("the last declared value will be used")
|
||||
.emit();
|
||||
} else {
|
||||
seen_keys.insert(kv.key().clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check: `data` should only be used with payload methods.
|
||||
if let Some(ref data_param) = data {
|
||||
if !method.node.supports_payload() {
|
||||
ecx.struct_span_warn(data_param.span, "`data` route parameter \
|
||||
used with non-payload-supporting method")
|
||||
.note(&format!("'{}' does not typically support payloads", method.node))
|
||||
.note("the 'format' attribute parameter will match against \
|
||||
the 'Accept' header")
|
||||
.emit();
|
||||
}
|
||||
}
|
||||
|
||||
RouteParams {
|
||||
method, uri, format, rank,
|
||||
data_param: data,
|
||||
query_param: query,
|
||||
annotated_fn: function,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_method(method: Method) -> bool {
|
||||
use rocket_http::Method::*;
|
||||
match method {
|
||||
Get | Put | Post | Delete | Head | Patch | Options => true,
|
||||
Trace | Connect => false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kv_from_nested(item: &NestedMetaItem) -> Option<KVSpanned<LitKind>> {
|
||||
item.name_value().map(|(name, value)| {
|
||||
let k_span = item.span().shorten_to(name.as_str().len());
|
||||
KVSpanned {
|
||||
key: span(name.to_string(), k_span),
|
||||
value: value.clone(),
|
||||
span: item.span(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn param_to_ident(ecx: &ExtCtxt, s: Spanned<&str>) -> Option<Spanned<Ident>> {
|
||||
let string = s.node;
|
||||
if string.starts_with('<') && string.ends_with('>') {
|
||||
let param = &string[1..(string.len() - 1)];
|
||||
if is_valid_ident(param) {
|
||||
return Some(span(Ident::from_str(param), s.span.trim(1)));
|
||||
}
|
||||
|
||||
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> {
|
||||
let default_method = dummy_spanned(Method::Get);
|
||||
let valid_methods = "valid methods are: `GET`, `PUT`, `POST`, `DELETE`, \
|
||||
`HEAD`, `PATCH`, `OPTIONS`";
|
||||
|
||||
if let Some(word) = meta_item.word() {
|
||||
if let Ok(method) = Method::from_str(&word.name().as_str()) {
|
||||
if is_valid_method(method) {
|
||||
return span(method, word.span());
|
||||
}
|
||||
}
|
||||
|
||||
let msg = format!("'{}' is not a valid method", word.ident);
|
||||
ecx.struct_span_err(word.span, &msg).help(valid_methods).emit();
|
||||
return default_method;
|
||||
}
|
||||
|
||||
// Fallthrough. Emit a generic error message and return default method.
|
||||
let msg = "expected a valid HTTP method identifier";
|
||||
ecx.struct_span_err(meta_item.span, msg).help(valid_methods).emit();
|
||||
dummy_spanned(Method::Get)
|
||||
}
|
||||
|
||||
fn parse_path(
|
||||
ecx: &ExtCtxt,
|
||||
meta_item: &NestedMetaItem
|
||||
) -> (Spanned<Origin<'static>>, Option<Spanned<Ident>>) {
|
||||
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 validate_uri(ecx, &s.as_str(), lit.span);
|
||||
} else {
|
||||
ecx.span_err(lit.span, "`path` value must be a string")
|
||||
}
|
||||
} else if let Some(s) = meta_item.str_lit() {
|
||||
return validate_uri(ecx, &s.as_str(), 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, \
|
||||
e.g: "/hello/world", or as a key-value pair, \
|
||||
e.g: path = "/hello/world" "#)
|
||||
.emit();
|
||||
}
|
||||
|
||||
(dummy_spanned(Origin::dummy()), None)
|
||||
}
|
||||
|
||||
fn parse_opt<O, T, F>(ecx: &ExtCtxt, kv: &KVSpanned<T>, f: F) -> Option<KVSpanned<O>>
|
||||
where F: Fn(&ExtCtxt, &KVSpanned<T>) -> O
|
||||
{
|
||||
Some(kv.map_ref(|_| f(ecx, kv)))
|
||||
}
|
||||
|
||||
fn parse_data(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> Ident {
|
||||
let mut ident = Ident::from_str("unknown");
|
||||
if let LitKind::Str(ref s, _) = *kv.value() {
|
||||
ident = Ident::from_str(&s.as_str());
|
||||
if let Some(id) = param_to_ident(ecx, span(&s.as_str(), kv.value.span)) {
|
||||
return id.node;
|
||||
}
|
||||
}
|
||||
|
||||
let err_string = r#"`data` value must be a parameter, e.g: "<name>"`"#;
|
||||
ecx.struct_span_fatal(kv.span, err_string)
|
||||
.help(r#"data, if specified, must be a key-value pair where
|
||||
the key is `data` and the value is a string with a single
|
||||
parameter inside '<' '>'. e.g: data = "<user_form>""#)
|
||||
.emit();
|
||||
|
||||
ident
|
||||
}
|
||||
|
||||
fn parse_rank(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> isize {
|
||||
if let LitKind::Int(n, _) = *kv.value() {
|
||||
let max = isize::max_value();
|
||||
if n <= max as u128 {
|
||||
return n as isize;
|
||||
} else {
|
||||
let msg = format!("rank must be less than or equal to {}", max);
|
||||
ecx.span_err(kv.value.span, msg.as_str());
|
||||
}
|
||||
} else {
|
||||
ecx.struct_span_err(kv.span, r#"`rank` value must be an int"#)
|
||||
.help(r#"the rank, if specified, must be a key-value pair where
|
||||
the key is `rank` and the value is an integer.
|
||||
e.g: rank = 1, or e.g: rank = 10"#)
|
||||
.emit();
|
||||
}
|
||||
|
||||
-1
|
||||
}
|
||||
|
||||
fn parse_format(ecx: &ExtCtxt, kv: &KVSpanned<LitKind>) -> MediaType {
|
||||
if let LitKind::Str(ref s, _) = *kv.value() {
|
||||
if let Some(ct) = MediaType::parse_flexible(&s.as_str()) {
|
||||
if !ct.is_known() {
|
||||
let msg = format!("'{}' is not a known media type", s);
|
||||
ecx.span_warn(kv.value.span, &msg);
|
||||
}
|
||||
|
||||
return ct;
|
||||
} else {
|
||||
ecx.span_err(kv.value.span, "malformed media type");
|
||||
}
|
||||
}
|
||||
|
||||
ecx.struct_span_err(kv.span, r#"`format` must be a "media/type""#)
|
||||
.help(r#"format, if specified, must be a key-value pair where
|
||||
the key is `format` and the value is a string representing the
|
||||
media type accepted. e.g: format = "application/json".
|
||||
shorthand is also accepted: format = "json"#)
|
||||
.emit();
|
||||
|
||||
MediaType::Any
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
use syntax::ast::*;
|
||||
use syntax::source_map::{Span, Spanned, dummy_spanned};
|
||||
use syntax::ext::base::ExtCtxt;
|
||||
|
||||
use rocket_http::ext::IntoOwned;
|
||||
use rocket_http::uri::{Uri, Origin};
|
||||
use super::route::param_to_ident;
|
||||
use utils::{span, SpanExt, is_valid_ident};
|
||||
|
||||
// We somewhat arbitrarily enforce absolute paths. This is mostly because we
|
||||
// want the initial "/" to represent the mount point. Empty segments are
|
||||
// stripped out at runtime. So, to avoid any confusion, we issue an error at
|
||||
// compile-time for empty segments. At the moment, this disallows trailing
|
||||
// slashes as well, since then the last segment is empty.
|
||||
fn valid_path(ecx: &ExtCtxt, uri: &Origin, sp: Span) -> bool {
|
||||
if !uri.is_normalized() {
|
||||
let normalized = uri.to_normalized();
|
||||
ecx.struct_span_err(sp, "paths cannot contain empty segments")
|
||||
.note(&format!("expected '{}', found '{}'", normalized, uri))
|
||||
.emit();
|
||||
}
|
||||
|
||||
uri.is_normalized()
|
||||
}
|
||||
|
||||
fn valid_segments(ecx: &ExtCtxt, uri: &Origin, sp: Span) -> bool {
|
||||
let mut validated = true;
|
||||
let mut segments_span = None;
|
||||
for segment in uri.segments() {
|
||||
// We add one to the index to account for the '/'.
|
||||
let index = segment.as_ptr() as usize - uri.path().as_ptr() as usize;
|
||||
let span = sp.trim_left(index + 1).shorten_to(segment.len());
|
||||
|
||||
// If we're iterating after a '..' param, that's a hard error.
|
||||
if let Some(span) = segments_span {
|
||||
let rem_sp = sp.trim_left(index).trim_right(1);
|
||||
ecx.struct_span_err(rem_sp, "text after a trailing '..' param")
|
||||
.help("a segments param must be the final text in a path")
|
||||
.span_note(span, "trailing param is here")
|
||||
.emit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this is a dynamic param. If so, check it's well-formedness.
|
||||
if segment.starts_with('<') && segment.ends_with('>') {
|
||||
let mut param = &segment[1..(segment.len() - 1)];
|
||||
if segment.ends_with("..>") {
|
||||
segments_span = Some(span);
|
||||
param = ¶m[..(param.len() - 2)];
|
||||
}
|
||||
|
||||
if param.is_empty() {
|
||||
ecx.span_err(span, "parameters cannot be empty");
|
||||
} else if !is_valid_ident(param) {
|
||||
ecx.struct_span_err(span, "parameter names must be valid identifiers")
|
||||
.note(&format!("{:?} is not a valid identifier", param))
|
||||
.emit();
|
||||
} else if param == "_" {
|
||||
ecx.struct_span_err(span, "parameters must be named")
|
||||
.help("use a name such as `_guard` or `_param`")
|
||||
.emit();
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
validated = false;
|
||||
} else if segment.starts_with('<') {
|
||||
if segment[1..].contains('<') || segment.contains('>') {
|
||||
ecx.struct_span_err(span, "malformed parameter")
|
||||
.help("parameters must be of the form '<param>'")
|
||||
.emit();
|
||||
} else {
|
||||
ecx.struct_span_err(span, "parameter is missing a closing bracket")
|
||||
.help(&format!("perhaps you meant '{}>'?", segment))
|
||||
.emit();
|
||||
}
|
||||
|
||||
validated = false;
|
||||
} else if Uri::percent_encode(segment) != segment {
|
||||
if segment.contains('<') || segment.contains('>') {
|
||||
ecx.struct_span_err(span, "malformed parameter")
|
||||
.help("parameters must be of the form '<param>'")
|
||||
.emit();
|
||||
} else {
|
||||
ecx.span_err(span, "segment contains invalid characters");
|
||||
}
|
||||
|
||||
validated = false;
|
||||
}
|
||||
}
|
||||
|
||||
validated
|
||||
}
|
||||
|
||||
pub fn validate_uri(
|
||||
ecx: &ExtCtxt,
|
||||
string: &str,
|
||||
sp: Span,
|
||||
) -> (Spanned<Origin<'static>>, Option<Spanned<Ident>>) {
|
||||
let query_param = string.find('?')
|
||||
.map(|i| span(&string[(i + 1)..], sp.trim_left(i + 1)))
|
||||
.and_then(|spanned_q_param| param_to_ident(ecx, spanned_q_param));
|
||||
|
||||
let dummy = (dummy_spanned(Origin::dummy()), query_param);
|
||||
match Origin::parse_route(string) {
|
||||
Ok(uri) => {
|
||||
let uri = uri.into_owned();
|
||||
if valid_segments(ecx, &uri, sp) && valid_path(ecx, &uri, sp) {
|
||||
return (span(uri, sp), query_param)
|
||||
}
|
||||
|
||||
dummy
|
||||
}
|
||||
Err(e) => {
|
||||
ecx.struct_span_err(sp, &format!("invalid path URI: {}", e))
|
||||
.note("expected path URI in origin form")
|
||||
.help("example: /path/<route>")
|
||||
.emit();
|
||||
|
||||
dummy
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
use syntax::ast::{Arg, PatKind, Ident, Name};
|
||||
|
||||
pub trait ArgExt {
|
||||
fn ident(&self) -> Option<&Ident>;
|
||||
|
||||
fn name(&self) -> Option<&Name> {
|
||||
self.ident().map(|ident| &ident.name)
|
||||
}
|
||||
|
||||
fn named(&self, name: &Name) -> bool {
|
||||
self.name().map_or(false, |a| a == name)
|
||||
}
|
||||
}
|
||||
|
||||
impl ArgExt for Arg {
|
||||
fn ident(&self) -> Option<&Ident> {
|
||||
match self.pat.node {
|
||||
PatKind::Ident(_, ref ident, _) => Some(&ident),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
use syntax::ast::{GenericParam, GenericParamKind};
|
||||
|
||||
pub trait GenericParamExt {
|
||||
/// Returns `true` if `self` is of kind `lifetime`.
|
||||
fn is_lifetime(&self) -> bool;
|
||||
}
|
||||
|
||||
impl GenericParamExt for GenericParam {
|
||||
fn is_lifetime(&self) -> bool {
|
||||
match self.kind {
|
||||
GenericParamKind::Lifetime => true,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
use syntax::ast::{LitKind, NestedMetaItem, MetaItemKind, Lit};
|
||||
use syntax::symbol::Symbol;
|
||||
|
||||
pub trait MetaItemExt {
|
||||
fn name_value(&self) -> Option<(Symbol, &Lit)>;
|
||||
fn str_lit(&self) -> Option<&Symbol>;
|
||||
fn int_lit(&self) -> Option<u128>;
|
||||
}
|
||||
|
||||
impl MetaItemExt for NestedMetaItem {
|
||||
fn name_value(&self) -> Option<(Symbol, &Lit)> {
|
||||
self.meta_item().and_then(|mi| match mi.node {
|
||||
MetaItemKind::NameValue(ref l) => Some((mi.name(), l)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn str_lit(&self) -> Option<&Symbol> {
|
||||
self.literal().and_then(|lit| match lit.node {
|
||||
LitKind::Str(ref s, _) => Some(s),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn int_lit(&self) -> Option<u128> {
|
||||
self.literal().and_then(|lit| match lit.node {
|
||||
LitKind::Int(n, _) => Some(n),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
mod meta_item_ext;
|
||||
mod arg_ext;
|
||||
mod parser_ext;
|
||||
mod ident_ext;
|
||||
mod span_ext;
|
||||
mod expr_ext;
|
||||
mod generics_ext;
|
||||
|
||||
pub use self::arg_ext::ArgExt;
|
||||
pub use self::meta_item_ext::MetaItemExt;
|
||||
pub use self::parser_ext::ParserExt;
|
||||
pub use self::ident_ext::IdentExt;
|
||||
pub use self::span_ext::SpanExt;
|
||||
pub use self::expr_ext::ExprExt;
|
||||
pub use self::generics_ext::GenericParamExt;
|
||||
|
||||
use std::convert::AsRef;
|
||||
|
||||
use syntax;
|
||||
use syntax::parse::token::Token;
|
||||
use syntax::tokenstream::TokenTree;
|
||||
use syntax::ast::{Item, Expr, Attribute, Ty};
|
||||
use syntax::ext::base::{Annotatable, ExtCtxt};
|
||||
use syntax::source_map::{Span, Spanned, DUMMY_SP};
|
||||
use syntax::ext::quote::rt::ToTokens;
|
||||
use syntax::print::pprust::item_to_string;
|
||||
use syntax::symbol::{Ident, Symbol};
|
||||
use syntax::fold::Folder;
|
||||
use syntax::attr::HasAttrs;
|
||||
use syntax::ptr::P;
|
||||
|
||||
macro_rules! debug {
|
||||
($($t:tt)*) => (
|
||||
// Enable debug logs if the DEBUG_ENV_VAR is set.
|
||||
if ::std::env::var(::DEBUG_ENV_VAR).is_ok() {
|
||||
eprintln!("--> {}:{} ({})", file!(), line!(), module_path!());
|
||||
eprintln!($($t)*);
|
||||
eprintln!();
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn span<T>(t: T, span: Span) -> Spanned<T> {
|
||||
Spanned { node: t, span }
|
||||
}
|
||||
|
||||
pub fn sep_by_tok<T>(ecx: &ExtCtxt, things: &[T], token: Token) -> Vec<TokenTree>
|
||||
where T: ToTokens
|
||||
{
|
||||
let mut output: Vec<TokenTree> = vec![];
|
||||
for (i, thing) in things.iter().enumerate() {
|
||||
output.extend(thing.to_tokens(ecx));
|
||||
if i < things.len() - 1 {
|
||||
output.push(TokenTree::Token(DUMMY_SP, token.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn option_as_expr<T: ToTokens>(ecx: &ExtCtxt, opt: &Option<T>) -> P<Expr> {
|
||||
match *opt {
|
||||
Some(ref item) => quote_expr!(ecx, Some($item)),
|
||||
None => quote_expr!(ecx, None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_item(items: &mut Vec<Annotatable>, item: P<Item>) {
|
||||
debug!("Emitting item:\n{}", item_to_string(&item));
|
||||
items.push(Annotatable::Item(item));
|
||||
}
|
||||
|
||||
pub fn attach_and_emit(out: &mut Vec<Annotatable>, attr: Attribute, to: Annotatable) {
|
||||
syntax::attr::mark_used(&attr);
|
||||
syntax::attr::mark_known(&attr);
|
||||
|
||||
// Attach the attribute to the user's function and emit it.
|
||||
if let Annotatable::Item(user_item) = to {
|
||||
let item = user_item.map_attrs(|mut attrs| {
|
||||
attrs.push(attr);
|
||||
attrs
|
||||
});
|
||||
|
||||
emit_item(out, item);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_as_tokens(ecx: &ExtCtxt, s: &str) -> Vec<TokenTree> {
|
||||
use syntax_pos::FileName;
|
||||
use syntax::parse::parse_stream_from_source_str as parse_stream;
|
||||
|
||||
parse_stream(FileName::ProcMacroSourceCode, s.into(), ecx.parse_sess, None)
|
||||
.into_trees()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub struct TyLifetimeRemover;
|
||||
|
||||
impl Folder for TyLifetimeRemover {
|
||||
fn fold_ident(&mut self, ident: Ident) -> Ident {
|
||||
if ident.as_str().starts_with('\'') {
|
||||
Ident::new(Symbol::intern("'_"), ident.span)
|
||||
} else {
|
||||
ident
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn strip_ty_lifetimes(ty: P<Ty>) -> P<Ty> {
|
||||
TyLifetimeRemover.fold_ty(ty)
|
||||
}
|
||||
|
||||
// Lifted from Rust's lexer, except this takes a `char`, not an `Option<char>`.
|
||||
fn ident_start(c: char) -> bool {
|
||||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' ||
|
||||
(c > '\x7f' && c.is_xid_start())
|
||||
}
|
||||
|
||||
// Lifted from Rust's lexer, except this takes a `char`, not an `Option<char>`.
|
||||
fn ident_continue(c: char) -> bool {
|
||||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
|
||||
c == '_' || (c > '\x7f' && c.is_xid_continue())
|
||||
}
|
||||
|
||||
pub fn is_valid_ident<S: AsRef<str>>(s: S) -> bool {
|
||||
let string = s.as_ref();
|
||||
if string.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i, c) in string.chars().enumerate() {
|
||||
if i == 0 {
|
||||
if !ident_start(c) {
|
||||
return false;
|
||||
}
|
||||
} else if !ident_continue(c) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
macro_rules! quote_enum {
|
||||
($ecx:expr, $var:expr => $(::$from_root:ident)+ -> $(::$to_root:ident)+
|
||||
{ $($variant:ident),+ ; $($extra:pat => $result:expr),* }) => ({
|
||||
use syntax::source_map::DUMMY_SP;
|
||||
use syntax::ast::Ident;
|
||||
use $(::$from_root)+::*;
|
||||
let root_idents = vec![$(Ident::from_str(stringify!($to_root))),+];
|
||||
match $var {
|
||||
$($variant => {
|
||||
let variant = Ident::from_str(stringify!($variant));
|
||||
let mut idents = root_idents.clone();
|
||||
idents.push(variant);
|
||||
$ecx.path_global(DUMMY_SP, idents)
|
||||
})+
|
||||
$($extra => $result),*
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::http::{Cookies, RawStr};
|
||||
use rocket::request::Form;
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct User<'a> {
|
||||
name: &'a RawStr,
|
||||
nickname: String,
|
||||
}
|
||||
|
||||
#[post("/<_name>?<_query>", format = "application/json", data = "<user>", rank = 2)]
|
||||
fn get(
|
||||
_name: &RawStr,
|
||||
_query: User,
|
||||
user: Form<User>,
|
||||
_cookies: Cookies
|
||||
) -> String {
|
||||
format!("{}:{}", user.name, user.nickname)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main() {
|
||||
let _ = routes![get];
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
#![feature(plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
#[post("/", format = "application/x-custom")]
|
||||
fn get() -> &'static str { "hi" }
|
||||
|
||||
#[test]
|
||||
fn main() { }
|
|
@ -1,16 +0,0 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
#[get("/test/<one>/<two>/<three>")]
|
||||
fn get(one: String, two: usize, three: isize) -> &'static str { "hi" }
|
||||
|
||||
#[get("/test/<_one>/<_two>/<__three>")]
|
||||
fn ignored(_one: String, _two: usize, __three: isize) -> &'static str { "hi" }
|
||||
|
||||
#[test]
|
||||
fn main() {
|
||||
let _ = routes![get, ignored];
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
#![feature(plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
#[get("/")]
|
||||
fn get() -> &'static str { "hi" }
|
||||
|
||||
#[get("/")]
|
||||
fn get_empty() { }
|
||||
|
||||
#[test]
|
||||
fn main() { }
|
|
@ -1,40 +0,0 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
#[get("/one")]
|
||||
fn one() { }
|
||||
|
||||
#[get("/two")]
|
||||
fn two() { }
|
||||
|
||||
#[get("/three")]
|
||||
fn three() { }
|
||||
|
||||
#[get("/four")]
|
||||
fn four() { }
|
||||
|
||||
#[test]
|
||||
fn main() {
|
||||
let instance = rocket::ignite()
|
||||
.mount("/", routes![one]);
|
||||
|
||||
let other = instance.mount("/", routes![two]);
|
||||
other.mount("/", routes![three])
|
||||
.mount("/", routes![four]);
|
||||
|
||||
rocket::ignite()
|
||||
.mount("/", routes![one])
|
||||
.mount("/", routes![two])
|
||||
.mount("/", routes![three])
|
||||
.mount("/", routes![four]);
|
||||
|
||||
let a = rocket::ignite()
|
||||
.mount("/", routes![one])
|
||||
.mount("/", routes![two]);
|
||||
|
||||
let b = a.mount("/", routes![three])
|
||||
.mount("/", routes![four]);
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
#[get("/<todo>")]
|
||||
fn todo(todo: String) -> String {
|
||||
todo
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main() {
|
||||
let _ = routes![todo];
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
#![feature(plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
#[get("/")] fn get() { }
|
||||
#[route(GET, "/")] fn get_r() { }
|
||||
|
||||
#[put("/")] fn put() { }
|
||||
#[route(PUT, "/")] fn put_r() { }
|
||||
|
||||
#[post("/")] fn post() { }
|
||||
#[route(POST, "/")] fn post_r() { }
|
||||
|
||||
#[delete("/")] fn delete() { }
|
||||
#[route(DELETE, "/")] fn delete_r() { }
|
||||
|
||||
#[head("/")] fn head() { }
|
||||
#[route(HEAD, "/")] fn head_r() { }
|
||||
|
||||
#[patch("/")] fn patch() { }
|
||||
#[route(PATCH, "/")] fn patch_r() { }
|
||||
|
||||
#[options("/")] fn options() { }
|
||||
#[route(OPTIONS, "/")] fn options_r() { }
|
||||
|
||||
#[test]
|
||||
fn main() { }
|
|
@ -1,16 +0,0 @@
|
|||
#![feature(plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
#[get("/", rank = 1)]
|
||||
fn get1() -> &'static str { "hi" }
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
fn get2() -> &'static str { "hi" }
|
||||
|
||||
#[get("/", rank = 3)]
|
||||
fn get3() -> &'static str { "hi" }
|
||||
|
||||
#[test]
|
||||
fn main() { }
|
|
@ -1,28 +0,0 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::{Rocket, State};
|
||||
|
||||
#[get("/")]
|
||||
fn index(state: State<u32>) { }
|
||||
|
||||
fn rocket() -> Rocket {
|
||||
rocket::ignite()
|
||||
.mount("/", routes![index])
|
||||
.manage(100u32)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main() {
|
||||
if false {
|
||||
rocket().launch();
|
||||
}
|
||||
|
||||
let instance = rocket();
|
||||
if false {
|
||||
instance.launch();
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
#![feature(plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use rocket::http::uri::SegmentError;
|
||||
|
||||
#[post("/<a>/<b..>")]
|
||||
fn get(a: String, b: PathBuf) -> String {
|
||||
format!("{}/{}", a, b.to_string_lossy())
|
||||
}
|
||||
|
||||
#[post("/<a>/<b..>")]
|
||||
fn get2(a: String, b: Result<PathBuf, SegmentError>) -> String {
|
||||
format!("{}/{}", a, b.unwrap().to_string_lossy())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main() { }
|
|
@ -1,25 +0,0 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::State;
|
||||
|
||||
type MyState<'r> = State<'r, usize>;
|
||||
|
||||
type MyVecState<'r, T> = State<'r, Vec<T>>;
|
||||
|
||||
#[get("/")]
|
||||
fn index(state: MyState) { }
|
||||
|
||||
#[get("/a")]
|
||||
fn another(state: MyVecState<usize>) { }
|
||||
|
||||
#[test]
|
||||
fn main() {
|
||||
rocket::ignite()
|
||||
.manage(10usize)
|
||||
.manage(vec![1usize, 2usize, 3usize])
|
||||
.mount("/", routes![index, another]);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(plugin, proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
@ -67,10 +67,10 @@ fn no_uri_display_okay(id: i32, form: Form<Second>) -> &'static str {
|
|||
"Typed URI testing."
|
||||
}
|
||||
|
||||
#[post("/<name>?<query>", data = "<user>", rank = 2)]
|
||||
#[post("/<name>?<query..>", data = "<user>", rank = 2)]
|
||||
fn complex<'r>(
|
||||
name: &RawStr,
|
||||
query: User<'r>,
|
||||
query: Form<User<'r>>,
|
||||
user: Form<User<'r>>,
|
||||
cookies: Cookies
|
||||
) -> &'static str { "" }
|
||||
|
|
|
@ -20,11 +20,14 @@ proc-macro = true
|
|||
indexmap = "1.0"
|
||||
quote = "0.6.1"
|
||||
rocket_http = { version = "0.4.0-dev", path = "../http/" }
|
||||
indexmap = "1"
|
||||
|
||||
[dependencies.derive_utils]
|
||||
git = "https://github.com/SergioBenitez/derive-utils"
|
||||
rev = "87ad56ba"
|
||||
path = "/Users/sbenitez/Sync/Data/Projects/Snippets/derive-utils/lib"
|
||||
# git = "https://github.com/SergioBenitez/derive-utils"
|
||||
# rev = "87ad56ba"
|
||||
|
||||
[dev-dependencies]
|
||||
rocket = { version = "0.4.0-dev", path = "../lib" }
|
||||
rocket_codegen = { version = "0.4.0-dev", path = "../codegen" }
|
||||
compiletest_rs = "0.3.14"
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro::{TokenStream, Span};
|
||||
use derive_utils::{syn, Spanned, Result, FromMeta};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use proc_macro::Span;
|
||||
|
||||
use http_codegen::Status;
|
||||
use syn_ext::{syn_to_diag, IdentExt, ReturnTypeExt};
|
||||
use self::syn::{Attribute, parse::Parser};
|
||||
|
||||
crate const CATCH_FN_PREFIX: &str = "rocket_catch_fn_";
|
||||
crate const CATCH_STRUCT_PREFIX: &str = "static_rocket_catch_info_for_";
|
||||
use {CATCH_FN_PREFIX, CATCH_STRUCT_PREFIX};
|
||||
|
||||
/// The raw, parsed `#[catch(code)]` attribute.
|
||||
#[derive(Debug, FromMeta)]
|
||||
|
@ -72,9 +69,8 @@ pub fn _catch(args: TokenStream, input: TokenStream) -> Result<TokenStream> {
|
|||
.unwrap_or(Span::call_site().into());
|
||||
|
||||
let catcher_response = quote_spanned!(return_type_span => {
|
||||
// Check the type signature.
|
||||
// Emit this to force a type signature check.
|
||||
let __catcher: #fn_sig = #user_catcher_fn_name;
|
||||
// Generate the response.
|
||||
::rocket::response::Responder::respond_to(__catcher(#inputs), __req)?
|
||||
});
|
||||
|
||||
|
@ -82,6 +78,7 @@ pub fn _catch(args: TokenStream, input: TokenStream) -> Result<TokenStream> {
|
|||
Ok(quote! {
|
||||
#user_catcher_fn
|
||||
|
||||
/// Rocket code generated wrapping catch function.
|
||||
#vis fn #generated_fn_name<'_b>(
|
||||
__req: &'_b ::rocket::Request
|
||||
) -> ::rocket::response::Result<'_b> {
|
||||
|
@ -92,6 +89,7 @@ pub fn _catch(args: TokenStream, input: TokenStream) -> Result<TokenStream> {
|
|||
.ok()
|
||||
}
|
||||
|
||||
/// Rocket code generated static catcher info.
|
||||
#[allow(non_upper_case_globals)]
|
||||
#vis static #generated_struct_name: ::rocket::StaticCatchInfo =
|
||||
::rocket::StaticCatchInfo {
|
||||
|
@ -102,11 +100,5 @@ pub fn _catch(args: TokenStream, input: TokenStream) -> Result<TokenStream> {
|
|||
}
|
||||
|
||||
pub fn catch_attribute(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
match _catch(args, input) {
|
||||
Ok(tokens) => tokens,
|
||||
Err(diag) => {
|
||||
diag.emit();
|
||||
TokenStream::new()
|
||||
}
|
||||
}
|
||||
_catch(args, input).unwrap_or_else(|d| { d.emit(); TokenStream::new() })
|
||||
}
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
pub mod catch;
|
||||
pub mod route;
|
||||
pub mod segments;
|
||||
|
|
|
@ -0,0 +1,445 @@
|
|||
use proc_macro::{TokenStream, Span};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use derive_utils::{syn, Spanned, Result, FromMeta, ext::TypeExt};
|
||||
use indexmap::IndexSet;
|
||||
|
||||
use proc_macro_ext::Diagnostics;
|
||||
use syn_ext::{syn_to_diag, IdentExt};
|
||||
use self::syn::{Attribute, parse::Parser};
|
||||
|
||||
use http_codegen::{Method, MediaType, RoutePath, DataSegment, Optional};
|
||||
use attribute::segments::{Source, Kind, Segment};
|
||||
use {ROUTE_FN_PREFIX, ROUTE_STRUCT_PREFIX, URI_MACRO_PREFIX, ROCKET_PARAM_PREFIX};
|
||||
|
||||
/// The raw, parsed `#[route]` attribute.
|
||||
#[derive(Debug, FromMeta)]
|
||||
struct RouteAttribute {
|
||||
#[meta(naked)]
|
||||
method: Method,
|
||||
path: RoutePath,
|
||||
data: Option<DataSegment>,
|
||||
format: Option<MediaType>,
|
||||
rank: Option<isize>,
|
||||
}
|
||||
|
||||
/// The raw, parsed `#[method]` (e.g, `get`, `put`, `post`, etc.) attribute.
|
||||
#[derive(Debug, FromMeta)]
|
||||
struct MethodRouteAttribute {
|
||||
#[meta(naked)]
|
||||
path: RoutePath,
|
||||
data: Option<DataSegment>,
|
||||
format: Option<MediaType>,
|
||||
rank: Option<isize>,
|
||||
}
|
||||
|
||||
/// This structure represents the parsed `route` attribute and associated items.
|
||||
#[derive(Debug)]
|
||||
struct Route {
|
||||
/// The status associated with the code in the `#[route(code)]` attribute.
|
||||
attribute: RouteAttribute,
|
||||
/// The function that was decorated with the `route` attribute.
|
||||
function: syn::ItemFn,
|
||||
/// The non-static parameters declared in the route segments.
|
||||
segments: IndexSet<Segment>,
|
||||
/// The parsed inputs to the user's function. The first ident is the ident
|
||||
/// as the user wrote it, while the second ident is the identifier that
|
||||
/// should be used during code generation, the `rocket_ident`.
|
||||
inputs: Vec<(syn::Ident, syn::Ident, syn::Type)>,
|
||||
}
|
||||
|
||||
fn parse_route(attr: RouteAttribute, function: syn::ItemFn) -> Result<Route> {
|
||||
// Gather diagnostics as we proceed.
|
||||
let mut diags = Diagnostics::new();
|
||||
|
||||
// Collect all of the dynamic segments in an `IndexSet`, checking for dups.
|
||||
let mut segments: IndexSet<Segment> = IndexSet::new();
|
||||
fn dup_check<I>(set: &mut IndexSet<Segment>, iter: I, diags: &mut Diagnostics)
|
||||
where I: Iterator<Item = Segment>
|
||||
{
|
||||
for segment in iter.filter(|s| s.kind != Kind::Static) {
|
||||
let span = segment.span;
|
||||
if let Some(previous) = set.replace(segment) {
|
||||
diags.push(span.error(format!("duplicate parameter: `{}`", previous.name))
|
||||
.span_note(previous.span, "previous parameter with the same name here"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dup_check(&mut segments, attr.path.path.iter().cloned(), &mut diags);
|
||||
attr.path.query.as_ref().map(|q| dup_check(&mut segments, q.iter().cloned(), &mut diags));
|
||||
dup_check(&mut segments, attr.data.clone().map(|s| s.0).into_iter(), &mut diags);
|
||||
|
||||
// Check the validity of function arguments.
|
||||
let mut inputs = vec![];
|
||||
let mut fn_segments: IndexSet<Segment> = IndexSet::new();
|
||||
for input in &function.decl.inputs {
|
||||
let help = "all handler arguments must be of the form: `ident: Type`";
|
||||
let span = input.span();
|
||||
let (ident, ty) = match input {
|
||||
syn::FnArg::Captured(arg) => match arg.pat {
|
||||
syn::Pat::Ident(ref pat) => (&pat.ident, &arg.ty),
|
||||
syn::Pat::Wild(_) => {
|
||||
diags.push(span.error("handler arguments cannot be ignored").help(help));
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
diags.push(span.error("invalid use of pattern").help(help));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Other cases shouldn't happen since we parsed an `ItemFn`.
|
||||
_ => {
|
||||
diags.push(span.error("invalid handler argument").help(help));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let rocket_ident = ident.prepend(ROCKET_PARAM_PREFIX);
|
||||
inputs.push((ident.clone(), rocket_ident, ty.with_stripped_lifetimes()));
|
||||
fn_segments.insert(ident.into());
|
||||
}
|
||||
|
||||
// Check that all of the declared parameters are function inputs.
|
||||
let span = function.decl.inputs.span();
|
||||
for missing in segments.difference(&fn_segments) {
|
||||
diags.push(missing.span.error("unused dynamic parameter")
|
||||
.span_note(span, format!("expected argument named `{}` here", missing.name)))
|
||||
}
|
||||
|
||||
diags.head_err_or(Route { attribute: attr, function, inputs, segments })
|
||||
}
|
||||
|
||||
fn param_expr(seg: &Segment, ident: &syn::Ident, ty: &syn::Type) -> TokenStream2 {
|
||||
let i = seg.index.expect("dynamic parameters must be indexed");
|
||||
let span = ident.span().unstable().join(ty.span()).unwrap().into();
|
||||
let name = ident.to_string();
|
||||
|
||||
// All dynamic parameter should be found if this function is being called;
|
||||
// that's the point of statically checking the URI parameters.
|
||||
let internal_error = quote!({
|
||||
log_error("Internal invariant error: expected dynamic parameter not found.");
|
||||
log_error("Please report this error to the Rocket issue tracker.");
|
||||
Outcome::Forward(__data)
|
||||
});
|
||||
|
||||
// Returned when a dynamic parameter fails to parse.
|
||||
let parse_error = quote!({
|
||||
log_warn_(&format!("Failed to parse '{}': {:?}", #name, __e));
|
||||
Outcome::Forward(__data)
|
||||
});
|
||||
|
||||
let expr = match seg.kind {
|
||||
Kind::Single => quote_spanned! { span =>
|
||||
match __req.raw_segment_str(#i) {
|
||||
Some(__s) => match <#ty as FromParam>::from_param(__s) {
|
||||
Ok(__v) => __v,
|
||||
Err(__e) => return #parse_error,
|
||||
},
|
||||
None => return #internal_error
|
||||
}
|
||||
},
|
||||
Kind::Multi => quote_spanned! { span =>
|
||||
match __req.raw_segments(#i) {
|
||||
Some(__s) => match <#ty as FromSegments>::from_segments(__s) {
|
||||
Ok(__v) => __v,
|
||||
Err(__e) => return #parse_error,
|
||||
},
|
||||
None => return #internal_error
|
||||
}
|
||||
},
|
||||
Kind::Static => return quote!()
|
||||
};
|
||||
|
||||
quote! {
|
||||
#[allow(non_snake_case, unreachable_patterns, unreachable_code)]
|
||||
let #ident: #ty = #expr;
|
||||
}
|
||||
}
|
||||
|
||||
fn data_expr(ident: &syn::Ident, ty: &syn::Type) -> TokenStream2 {
|
||||
let span = ident.span().unstable().join(ty.span()).unwrap().into();
|
||||
quote_spanned! { span =>
|
||||
let __transform = <#ty as FromData>::transform(__req, __data);
|
||||
|
||||
#[allow(unreachable_patterns, unreachable_code)]
|
||||
let __outcome = match __transform {
|
||||
Owned(Outcome::Success(__v)) => Owned(Outcome::Success(__v)),
|
||||
Borrowed(Outcome::Success(ref __v)) => {
|
||||
Borrowed(Outcome::Success(::std::borrow::Borrow::borrow(__v)))
|
||||
}
|
||||
Borrowed(__o) => Borrowed(__o.map(|_| loop { /* unreachable */ })),
|
||||
Owned(__o) => Owned(__o)
|
||||
};
|
||||
|
||||
#[allow(non_snake_case, unreachable_patterns, unreachable_code)]
|
||||
let #ident: #ty = match <#ty as FromData>::from_data(__req, __outcome) {
|
||||
Outcome::Success(__d) => __d,
|
||||
Outcome::Forward(__d) => return Outcome::Forward(__d),
|
||||
Outcome::Failure((__c, _)) => return Outcome::Failure(__c),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn query_exprs(route: &Route) -> Option<TokenStream2> {
|
||||
let query_segments = route.attribute.path.query.as_ref()?;
|
||||
let (mut decls, mut matchers, mut builders) = (vec![], vec![], vec![]);
|
||||
for segment in query_segments {
|
||||
let name = &segment.name;
|
||||
let (ident, ty, span) = if segment.kind != Kind::Static {
|
||||
let (ident, ty) = route.inputs.iter()
|
||||
.find(|(ident, _, _)| ident == &segment.name)
|
||||
.map(|(_, rocket_ident, ty)| (rocket_ident, ty))
|
||||
.unwrap();
|
||||
|
||||
let span = ident.span().unstable().join(ty.span()).unwrap();
|
||||
(Some(ident), Some(ty), span.into())
|
||||
} else {
|
||||
(None, None, segment.span.into())
|
||||
};
|
||||
|
||||
let decl = match segment.kind {
|
||||
Kind::Single => quote_spanned! { span =>
|
||||
let mut #ident: Option<#ty> = None;
|
||||
},
|
||||
Kind::Multi => quote_spanned! { span =>
|
||||
let mut __trail = SmallVec::<[FormItem; 8]>::new();
|
||||
},
|
||||
Kind::Static => quote!()
|
||||
};
|
||||
|
||||
let matcher = match segment.kind {
|
||||
Kind::Single => quote_spanned! { span =>
|
||||
(_, #name, __v) => {
|
||||
#ident = Some(match <#ty as FromFormValue>::from_form_value(__v) {
|
||||
Ok(__v) => __v,
|
||||
Err(__e) => {
|
||||
log_warn_(&format!("Failed to parse '{}': {:?}", #name, __e));
|
||||
return Outcome::Forward(__data);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
Kind::Static => quote! {
|
||||
(#name, _, _) => continue,
|
||||
},
|
||||
Kind::Multi => quote! {
|
||||
_ => __trail.push(__i),
|
||||
}
|
||||
};
|
||||
|
||||
let builder = match segment.kind {
|
||||
Kind::Single => quote_spanned! { span =>
|
||||
let #ident = match #ident.or_else(<#ty as FromFormValue>::default) {
|
||||
Some(__v) => __v,
|
||||
None => {
|
||||
log_warn_(&format!("Missing required query parameter '{}'.", #name));
|
||||
return Outcome::Forward(__data);
|
||||
}
|
||||
};
|
||||
},
|
||||
Kind::Multi => quote_spanned! { span =>
|
||||
let #ident = match <#ty as FromQuery>::from_query(Query(&__trail)) {
|
||||
Ok(__v) => __v,
|
||||
Err(__e) => {
|
||||
log_warn_(&format!("Failed to parse '{}': {:?}", #name, __e));
|
||||
return Outcome::Forward(__data);
|
||||
}
|
||||
};
|
||||
},
|
||||
Kind::Static => quote!()
|
||||
};
|
||||
|
||||
decls.push(decl);
|
||||
matchers.push(matcher);
|
||||
builders.push(builder);
|
||||
}
|
||||
|
||||
matchers.push(quote!(_ => continue));
|
||||
Some(quote! {
|
||||
#(#decls)*
|
||||
|
||||
if let Some(__items) = __req.raw_query_items() {
|
||||
for __i in __items {
|
||||
match (__i.raw.as_str(), __i.key.as_str(), __i.value) {
|
||||
#(
|
||||
#[allow(unreachable_patterns, unreachable_code)]
|
||||
#matchers
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#(
|
||||
#[allow(unreachable_patterns, unreachable_code)]
|
||||
#builders
|
||||
)*
|
||||
})
|
||||
}
|
||||
|
||||
fn request_guard_expr(ident: &syn::Ident, ty: &syn::Type) -> TokenStream2 {
|
||||
let span = ident.span().unstable().join(ty.span()).unwrap().into();
|
||||
quote_spanned! { span =>
|
||||
#[allow(non_snake_case, unreachable_patterns, unreachable_code)]
|
||||
let #ident: #ty = match <#ty as FromRequest>::from_request(__req) {
|
||||
Outcome::Success(__v) => __v,
|
||||
Outcome::Forward(_) => return Outcome::Forward(__data),
|
||||
Outcome::Failure((__c, _)) => return Outcome::Failure(__c),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_internal_uri_macro(route: &Route) -> TokenStream2 {
|
||||
let dynamic_args = route.segments.iter()
|
||||
.filter(|seg| seg.source == Source::Path || seg.source == Source::Query)
|
||||
.filter(|seg| seg.kind != Kind::Static)
|
||||
.map(|seg| &seg.name)
|
||||
.map(|name| route.inputs.iter().find(|(ident, ..)| ident == name).unwrap())
|
||||
.map(|(ident, _, ty)| quote!(#ident: #ty));
|
||||
|
||||
let generated_macro_name = route.function.ident.prepend(URI_MACRO_PREFIX);
|
||||
let route_uri = route.attribute.path.origin.0.to_string();
|
||||
|
||||
quote! {
|
||||
pub macro #generated_macro_name($($token:tt)*) {
|
||||
rocket_internal_uri!(#route_uri, (#(#dynamic_args),*), $($token)*)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn codegen_route(route: Route) -> Result<TokenStream> {
|
||||
// Generate the declarations for path, data, and request guard parameters.
|
||||
let mut data_stmt = None;
|
||||
let mut parameter_definitions = vec![];
|
||||
for (ident, rocket_ident, ty) in &route.inputs {
|
||||
let fn_segment: Segment = ident.into();
|
||||
let parameter_def = match route.segments.get(&fn_segment) {
|
||||
Some(seg) if seg.source == Source::Path => {
|
||||
param_expr(seg, rocket_ident, &ty)
|
||||
}
|
||||
Some(seg) if seg.source == Source::Data => {
|
||||
// the data statement needs to come last, so record it specially
|
||||
data_stmt = Some(data_expr(rocket_ident, &ty));
|
||||
continue;
|
||||
}
|
||||
// handle query parameters later
|
||||
Some(_) => continue,
|
||||
None => request_guard_expr(rocket_ident, &ty),
|
||||
};
|
||||
|
||||
parameter_definitions.push(parameter_def);
|
||||
}
|
||||
|
||||
// Generate the declarations for query parameters.
|
||||
if let Some(exprs) = query_exprs(&route) {
|
||||
parameter_definitions.push(exprs);
|
||||
}
|
||||
|
||||
// Gather everything we need.
|
||||
let (vis, user_handler_fn) = (&route.function.vis, &route.function);
|
||||
let user_handler_fn_name = &user_handler_fn.ident;
|
||||
let generated_fn_name = user_handler_fn_name.prepend(ROUTE_FN_PREFIX);
|
||||
let generated_struct_name = user_handler_fn_name.prepend(ROUTE_STRUCT_PREFIX);
|
||||
let parameter_names = route.inputs.iter().map(|(_, rocket_ident, _)| rocket_ident);
|
||||
let generated_internal_uri_macro = generate_internal_uri_macro(&route);
|
||||
let method = route.attribute.method;
|
||||
let path = route.attribute.path.origin.0.to_string();
|
||||
let rank = Optional(route.attribute.rank);
|
||||
let format = Optional(route.attribute.format);
|
||||
|
||||
Ok(quote! {
|
||||
#user_handler_fn
|
||||
|
||||
/// Rocket code generated wrapping route function.
|
||||
#vis fn #generated_fn_name<'_b>(
|
||||
__req: &'_b ::rocket::Request,
|
||||
__data: ::rocket::Data
|
||||
) -> ::rocket::handler::Outcome<'_b> {
|
||||
#[allow(unused_imports)]
|
||||
use rocket::{
|
||||
handler, Outcome,
|
||||
logger::{log_warn, log_error, log_warn_},
|
||||
data::{FromData, Transform::*},
|
||||
http::{SmallVec, RawStr},
|
||||
request::{FromRequest, FromParam, FromFormValue, FromSegments},
|
||||
request::{Query, FromQuery, FormItems, FormItem},
|
||||
};
|
||||
|
||||
#(#parameter_definitions)*
|
||||
#data_stmt
|
||||
|
||||
let ___responder = #user_handler_fn_name(#(#parameter_names),*);
|
||||
handler::Outcome::from(__req, ___responder)
|
||||
}
|
||||
|
||||
/// Rocket code generated wrapping URI macro.
|
||||
#generated_internal_uri_macro
|
||||
|
||||
/// Rocket code generated static route info.
|
||||
#[allow(non_upper_case_globals)]
|
||||
#vis static #generated_struct_name: ::rocket::StaticRouteInfo =
|
||||
::rocket::StaticRouteInfo {
|
||||
name: stringify!(#user_handler_fn_name),
|
||||
method: #method,
|
||||
path: #path,
|
||||
handler: #generated_fn_name,
|
||||
format: #format,
|
||||
rank: #rank,
|
||||
};
|
||||
}.into())
|
||||
}
|
||||
|
||||
fn complete_route(args: TokenStream2, input: TokenStream) -> Result<TokenStream> {
|
||||
let function: syn::ItemFn = syn::parse(input).map_err(syn_to_diag)
|
||||
.map_err(|diag| diag.help("`#[route]` can only be used on functions"))?;
|
||||
|
||||
let full_attr = quote!(#[route(#args)]);
|
||||
let attrs = Attribute::parse_outer.parse2(full_attr).map_err(syn_to_diag)?;
|
||||
let attribute = match RouteAttribute::from_attrs("route", &attrs) {
|
||||
Some(result) => result?,
|
||||
None => return Err(Span::call_site().error("internal error: bad attribute"))
|
||||
};
|
||||
|
||||
codegen_route(parse_route(attribute, function)?)
|
||||
}
|
||||
|
||||
fn incomplete_route(
|
||||
method: ::http::Method,
|
||||
args: TokenStream2,
|
||||
input: TokenStream
|
||||
) -> Result<TokenStream> {
|
||||
let method_str = method.to_string().to_lowercase();
|
||||
let method_ident = syn::Ident::new(&method_str, args.span().into());
|
||||
|
||||
let function: syn::ItemFn = syn::parse(input).map_err(syn_to_diag)
|
||||
.map_err(|d| d.help(format!("#[{}] can only be used on functions", method_str)))?;
|
||||
|
||||
let full_attr = quote!(#[#method_ident(#args)]);
|
||||
let attrs = Attribute::parse_outer.parse2(full_attr).map_err(syn_to_diag)?;
|
||||
let method_attribute = match MethodRouteAttribute::from_attrs(&method_str, &attrs) {
|
||||
Some(result) => result?,
|
||||
None => return Err(Span::call_site().error("internal error: bad attribute"))
|
||||
};
|
||||
|
||||
let attribute = RouteAttribute {
|
||||
method: Method(method),
|
||||
path: method_attribute.path,
|
||||
data: method_attribute.data,
|
||||
format: method_attribute.format,
|
||||
rank: method_attribute.rank,
|
||||
};
|
||||
|
||||
codegen_route(parse_route(attribute, function)?)
|
||||
}
|
||||
|
||||
pub fn route_attribute<M: Into<Option<::http::Method>>>(
|
||||
method: M,
|
||||
args: TokenStream,
|
||||
input: TokenStream
|
||||
) -> TokenStream {
|
||||
let result = match method.into() {
|
||||
Some(method) => incomplete_route(method, args.into(), input),
|
||||
None => complete_route(args.into(), input)
|
||||
};
|
||||
|
||||
result.unwrap_or_else(|diag| { diag.emit(); TokenStream::new() })
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use derive_utils::syn;
|
||||
use proc_macro::{Span, Diagnostic};
|
||||
|
||||
use http::route::RouteSegment;
|
||||
use proc_macro_ext::{SpanExt, Diagnostics, PResult, DResult};
|
||||
|
||||
pub use http::route::{Error, Kind, Source};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Segment {
|
||||
pub span: Span,
|
||||
pub kind: Kind,
|
||||
pub source: Source,
|
||||
pub name: String,
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
|
||||
impl Segment {
|
||||
fn from(segment: RouteSegment, span: Span) -> Segment {
|
||||
let (kind, source, index) = (segment.kind, segment.source, segment.index);
|
||||
Segment { span, kind, source, index, name: segment.name.into_owned() }
|
||||
}
|
||||
|
||||
crate fn to_route_segment<'a>(&'a self) -> String {
|
||||
match (self.source, self.kind) {
|
||||
(_, Kind::Single) => format!("<{}>", self.name),
|
||||
(_, Kind::Multi) => format!("<{}..>", self.name),
|
||||
(_, Kind::Static) => self.name.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a syn::Ident> for Segment {
|
||||
fn from(ident: &syn::Ident) -> Segment {
|
||||
Segment {
|
||||
kind: Kind::Static,
|
||||
source: Source::Unknown,
|
||||
span: ident.span().unstable(),
|
||||
name: ident.to_string(),
|
||||
index: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Segment {
|
||||
fn eq(&self, other: &Segment) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Segment { }
|
||||
|
||||
impl Hash for Segment {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subspan(needle: &str, haystack: &str, span: Span) -> Option<Span> {
|
||||
let index = needle.as_ptr() as usize - haystack.as_ptr() as usize;
|
||||
let remaining = haystack.len() - (index + needle.len());
|
||||
span.trimmed(index, remaining)
|
||||
}
|
||||
|
||||
pub fn trailspan(needle: &str, haystack: &str, span: Span) -> Option<Span> {
|
||||
let index = needle.as_ptr() as usize - haystack.as_ptr() as usize;
|
||||
span.trimmed(index - 1, 0)
|
||||
}
|
||||
|
||||
pub fn into_diagnostic(
|
||||
segment: &str, // The segment that failed.
|
||||
source: &str, // The haystack where `segment` can be found.
|
||||
span: Span, // The `Span` of `Source`.
|
||||
error: &Error, // The error.
|
||||
) -> Diagnostic {
|
||||
let seg_span = subspan(segment, source, span).unwrap();
|
||||
match error {
|
||||
Error::Empty => {
|
||||
seg_span.error("parameter names cannot be empty")
|
||||
}
|
||||
Error::Ident(name) => {
|
||||
seg_span.error(format!("`{}` is not a valid identifier", name))
|
||||
.help("parameter names must be valid identifiers")
|
||||
}
|
||||
Error::Ignored => {
|
||||
seg_span.error("parameters must be named")
|
||||
.help("use a name such as `_guard` or `_param`")
|
||||
}
|
||||
Error::MissingClose => {
|
||||
seg_span.error("parameter is missing a closing bracket")
|
||||
.help(format!("did you mean '{}>'?", segment))
|
||||
}
|
||||
Error::Malformed => {
|
||||
seg_span.error("malformed parameter or identifier")
|
||||
.help("parameters must be of the form '<param>'")
|
||||
.help("identifiers cannot contain '<' or '>'")
|
||||
}
|
||||
Error::Uri => {
|
||||
seg_span.error("component contains invalid URI characters")
|
||||
.note("components cannot contain '%' and '+' characters")
|
||||
}
|
||||
Error::Trailing(multi) => {
|
||||
let multi_span = subspan(multi, source, span).unwrap();
|
||||
trailspan(segment, source, span).unwrap()
|
||||
.error("unexpected trailing text after a '..' param")
|
||||
.help("a multi-segment param must be the final component")
|
||||
.span_note(multi_span, "multi-segment param is here")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_segment(segment: &str, span: Span) -> PResult<Segment> {
|
||||
RouteSegment::parse_one(segment)
|
||||
.map(|segment| Segment::from(segment, span))
|
||||
.map_err(|e| into_diagnostic(segment, segment, span, &e))
|
||||
}
|
||||
|
||||
pub fn parse_segments(
|
||||
string: &str,
|
||||
sep: char,
|
||||
source: Source,
|
||||
span: Span
|
||||
) -> DResult<Vec<Segment>> {
|
||||
let mut segments = vec![];
|
||||
let mut diags = Diagnostics::new();
|
||||
|
||||
for result in RouteSegment::parse_many(string, sep, source) {
|
||||
if let Err((segment_string, error)) = result {
|
||||
diags.push(into_diagnostic(segment_string, string, span, &error));
|
||||
if let Error::Trailing(..) = error {
|
||||
break;
|
||||
}
|
||||
} else if let Ok(segment) = result {
|
||||
let seg_span = subspan(&segment.string, string, span).unwrap();
|
||||
segments.push(Segment::from(segment, seg_span));
|
||||
}
|
||||
}
|
||||
|
||||
diags.err_or(segments)
|
||||
}
|
|
@ -4,17 +4,21 @@ use proc_macro2::TokenStream as TokenStream2;
|
|||
use derive_utils::{syn, Spanned, Result};
|
||||
use self::syn::{Path, punctuated::Punctuated, parse::Parser, token::Comma};
|
||||
use syn_ext::{IdentExt, syn_to_diag};
|
||||
use {ROUTE_STRUCT_PREFIX, CATCH_STRUCT_PREFIX};
|
||||
|
||||
mod uri;
|
||||
mod uri_parsing;
|
||||
|
||||
|
||||
crate fn prefix_last_segment(path: &mut Path, prefix: &str) {
|
||||
let mut last_seg = path.segments.last_mut().expect("syn::Path has segments");
|
||||
last_seg.value_mut().ident = last_seg.value().ident.prepend(prefix);
|
||||
}
|
||||
|
||||
fn _prefixed_vec(prefix: &str, input: TokenStream, ty: &TokenStream2) -> Result<TokenStream2> {
|
||||
fn _prefixed_vec(
|
||||
prefix: &str,
|
||||
input: TokenStream,
|
||||
ty: &TokenStream2
|
||||
) -> Result<TokenStream2> {
|
||||
// Parse a comma-separated list of paths.
|
||||
let mut paths = <Punctuated<Path, Comma>>::parse_terminated
|
||||
.parse(input)
|
||||
|
@ -41,14 +45,10 @@ fn prefixed_vec(prefix: &str, input: TokenStream, ty: TokenStream2) -> TokenStre
|
|||
}).into()
|
||||
}
|
||||
|
||||
pub static ROUTE_STRUCT_PREFIX: &str = "static_rocket_route_info_for_";
|
||||
|
||||
pub fn routes_macro(input: TokenStream) -> TokenStream {
|
||||
prefixed_vec(ROUTE_STRUCT_PREFIX, input, quote!(::rocket::Route))
|
||||
}
|
||||
|
||||
pub static CATCH_STRUCT_PREFIX: &str = "static_rocket_catch_info_for_";
|
||||
|
||||
pub fn catchers_macro(input: TokenStream) -> TokenStream {
|
||||
prefixed_vec(CATCH_STRUCT_PREFIX, input, quote!(::rocket::Catcher))
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ pub fn derive_from_form(input: TokenStream) -> TokenStream {
|
|||
Ok(quote! {
|
||||
#(#constructors)*
|
||||
|
||||
for (__k, __v) in __items {
|
||||
for (__k, __v) in __items.map(|item| item.key_value()) {
|
||||
match __k.as_str() {
|
||||
#(#matchers)*
|
||||
_ if __strict && __k != "_method" => {
|
||||
|
|
|
@ -1,16 +1,38 @@
|
|||
use quote::ToTokens;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use derive_utils::{FromMeta, MetaItem, Result, ext::Split2};
|
||||
use rocket_http as http;
|
||||
use http::{self, ext::IntoOwned};
|
||||
use attribute::segments::{parse_segments, parse_segment, Segment, Kind, Source};
|
||||
|
||||
use proc_macro_ext::SpanExt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ContentType(http::ContentType);
|
||||
pub struct ContentType(pub http::ContentType);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Status(pub http::Status);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MediaType(http::MediaType);
|
||||
pub struct MediaType(pub http::MediaType);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Method(pub http::Method);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Origin(pub http::uri::Origin<'static>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DataSegment(pub Segment);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Optional<T>(pub Option<T>);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RoutePath {
|
||||
pub origin: Origin,
|
||||
pub path: Vec<Segment>,
|
||||
pub query: Option<Vec<Segment>>,
|
||||
}
|
||||
|
||||
impl FromMeta for Status {
|
||||
fn from_meta(meta: MetaItem) -> Result<Self> {
|
||||
|
@ -34,7 +56,7 @@ impl FromMeta for ContentType {
|
|||
fn from_meta(meta: MetaItem) -> Result<Self> {
|
||||
http::ContentType::parse_flexible(&String::from_meta(meta)?)
|
||||
.map(ContentType)
|
||||
.ok_or(meta.value_span().error("invalid or unknown content-type"))
|
||||
.ok_or(meta.value_span().error("invalid or unknown content type"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +68,14 @@ impl ToTokens for ContentType {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromMeta for MediaType {
|
||||
fn from_meta(meta: MetaItem) -> Result<Self> {
|
||||
http::MediaType::parse_flexible(&String::from_meta(meta)?)
|
||||
.map(MediaType)
|
||||
.ok_or(meta.value_span().error("invalid or unknown media type"))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for MediaType {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
use std::iter::repeat;
|
||||
|
@ -70,3 +100,141 @@ impl ToTokens for MediaType {
|
|||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const VALID_METHODS_STR: &str = "`GET`, `PUT`, `POST`, `DELETE`, `HEAD`, \
|
||||
`PATCH`, `OPTIONS`";
|
||||
|
||||
const VALID_METHODS: &[http::Method] = &[
|
||||
http::Method::Get, http::Method::Put, http::Method::Post,
|
||||
http::Method::Delete, http::Method::Head, http::Method::Patch,
|
||||
http::Method::Options,
|
||||
];
|
||||
|
||||
impl FromMeta for Method {
|
||||
fn from_meta(meta: MetaItem) -> Result<Self> {
|
||||
let span = meta.value_span();
|
||||
let help_text = format!("method must be one of: {}", VALID_METHODS_STR);
|
||||
|
||||
if let MetaItem::Ident(ident) = meta {
|
||||
let method = ident.to_string().parse()
|
||||
.map_err(|_| span.error("invalid HTTP method").help(&*help_text))?;
|
||||
|
||||
if !VALID_METHODS.contains(&method) {
|
||||
return Err(span.error("invalid HTTP method for route handlers")
|
||||
.help(&*help_text));
|
||||
}
|
||||
|
||||
return Ok(Method(method));
|
||||
}
|
||||
|
||||
Err(span.error(format!("expected identifier, found {}", meta.description()))
|
||||
.help(&*help_text))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Method {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let method_tokens = match self.0 {
|
||||
http::Method::Get => quote!(::rocket::http::Method::Get),
|
||||
http::Method::Put => quote!(::rocket::http::Method::Put),
|
||||
http::Method::Post => quote!(::rocket::http::Method::Post),
|
||||
http::Method::Delete => quote!(::rocket::http::Method::Delete),
|
||||
http::Method::Options => quote!(::rocket::http::Method::Options),
|
||||
http::Method::Head => quote!(::rocket::http::Method::Head),
|
||||
http::Method::Trace => quote!(::rocket::http::Method::Trace),
|
||||
http::Method::Connect => quote!(::rocket::http::Method::Connect),
|
||||
http::Method::Patch => quote!(::rocket::http::Method::Patch),
|
||||
};
|
||||
|
||||
tokens.extend(method_tokens);
|
||||
}
|
||||
}
|
||||
|
||||
impl FromMeta for Origin {
|
||||
fn from_meta(meta: MetaItem) -> Result<Self> {
|
||||
let string = String::from_meta(meta)?;
|
||||
let span = meta.value_span();
|
||||
|
||||
let uri = http::uri::Origin::parse_route(&string)
|
||||
.map_err(|e| {
|
||||
let span = e.index()
|
||||
.map(|i| span.trimmed(i + 1, 0).unwrap())
|
||||
.unwrap_or(span);
|
||||
|
||||
span.error(format!("invalid path URI: {}", e))
|
||||
.help("expected path in origin form: \"/path/<param>\"")
|
||||
})?;
|
||||
|
||||
if !uri.is_normalized() {
|
||||
let normalized = uri.to_normalized();
|
||||
return Err(span.error("paths cannot contain empty segments")
|
||||
.note(format!("expected '{}', found '{}'", normalized, uri)));
|
||||
}
|
||||
|
||||
Ok(Origin(uri.into_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromMeta for Segment {
|
||||
fn from_meta(meta: MetaItem) -> Result<Self> {
|
||||
let string = String::from_meta(meta)?;
|
||||
let span = meta.value_span().trimmed(1, 1).unwrap();
|
||||
|
||||
let segment = parse_segment(&string, span)?;
|
||||
if segment.kind != Kind::Single {
|
||||
return Err(span.error("malformed parameter")
|
||||
.help("parameter must be of the form '<param>'"));
|
||||
}
|
||||
|
||||
Ok(segment)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromMeta for DataSegment {
|
||||
fn from_meta(meta: MetaItem) -> Result<Self> {
|
||||
let mut segment = Segment::from_meta(meta)?;
|
||||
segment.source = Source::Data;
|
||||
segment.index = Some(0);
|
||||
Ok(DataSegment(segment))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromMeta for RoutePath {
|
||||
fn from_meta(meta: MetaItem) -> Result<Self> {
|
||||
let origin = Origin::from_meta(meta)?;
|
||||
|
||||
let span = meta.value_span().trimmed(1, 1).unwrap();
|
||||
let query_len = origin.0.query().map(|q| q.len() + 1).unwrap_or(0);
|
||||
let path_span = span.trimmed(0, query_len).unwrap();
|
||||
let path = parse_segments(origin.0.path(), '/', Source::Path, path_span);
|
||||
|
||||
let query = origin.0.query()
|
||||
.map(|q| {
|
||||
let len_to_q = origin.0.path().len() + 1;
|
||||
let query_span = span.trimmed(len_to_q, 0).unwrap();
|
||||
if q.starts_with('&') || q.contains("&&") || q.ends_with('&') {
|
||||
// TODO: Show a help message with what's expected.
|
||||
Err(query_span.error("query cannot contain empty components").into())
|
||||
} else {
|
||||
parse_segments(q, '&', Source::Query, query_span)
|
||||
}
|
||||
}).transpose();
|
||||
|
||||
match (path, query) {
|
||||
(Ok(path), Ok(query)) => Ok(RoutePath { origin, path, query }),
|
||||
(Err(diag), Ok(_)) | (Ok(_), Err(diag)) => Err(diag.emit_head()),
|
||||
(Err(d1), Err(d2)) => Err(d1.join(d2).emit_head())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToTokens> ToTokens for Optional<T> {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let opt_tokens = match self.0 {
|
||||
Some(ref val) => quote!(Some(#val)),
|
||||
None => quote!(None)
|
||||
};
|
||||
|
||||
tokens.extend(opt_tokens);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,52 @@
|
|||
#![feature(proc_macro_diagnostic, proc_macro_span)]
|
||||
#![feature(crate_visibility_modifier)]
|
||||
#![feature(transpose_result)]
|
||||
#![feature(rustc_private)]
|
||||
#![recursion_limit="128"]
|
||||
|
||||
#[macro_use] extern crate quote;
|
||||
#[macro_use] extern crate derive_utils;
|
||||
extern crate indexmap;
|
||||
extern crate proc_macro;
|
||||
extern crate rocket_http;
|
||||
extern crate rocket_http as http;
|
||||
extern crate indexmap;
|
||||
|
||||
extern crate syntax_pos;
|
||||
|
||||
#[macro_use] mod proc_macro_ext;
|
||||
mod derive;
|
||||
mod attribute;
|
||||
mod bang;
|
||||
mod http_codegen;
|
||||
mod syn_ext;
|
||||
|
||||
use http::Method;
|
||||
use proc_macro::TokenStream;
|
||||
crate use derive_utils::proc_macro2;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
crate static ROUTE_STRUCT_PREFIX: &str = "static_rocket_route_info_for_";
|
||||
crate static CATCH_STRUCT_PREFIX: &str = "static_rocket_catch_info_for_";
|
||||
crate static CATCH_FN_PREFIX: &str = "rocket_catch_fn_";
|
||||
crate static ROUTE_FN_PREFIX: &str = "rocket_route_fn_";
|
||||
crate static URI_MACRO_PREFIX: &str = "rocket_uri_macro_";
|
||||
crate static ROCKET_PARAM_PREFIX: &str = "__rocket_param_";
|
||||
|
||||
macro_rules! route_attribute {
|
||||
($name:ident => $method:expr) => (
|
||||
#[proc_macro_attribute]
|
||||
pub fn $name(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
attribute::route::route_attribute($method, args, input)
|
||||
}
|
||||
)
|
||||
}
|
||||
route_attribute!(route => None);
|
||||
route_attribute!(get => Method::Get);
|
||||
route_attribute!(put => Method::Put);
|
||||
route_attribute!(post => Method::Post);
|
||||
route_attribute!(delete => Method::Delete);
|
||||
route_attribute!(head => Method::Head);
|
||||
route_attribute!(patch => Method::Patch);
|
||||
route_attribute!(options => Method::Options);
|
||||
|
||||
#[proc_macro_derive(FromFormValue, attributes(form))]
|
||||
pub fn derive_from_form_value(input: TokenStream) -> TokenStream {
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
use proc_macro::{Span, Diagnostic, /* MultiSpan */};
|
||||
use syntax_pos::{Span as InnerSpan, Pos, BytePos};
|
||||
|
||||
pub type PResult<T> = ::std::result::Result<T, Diagnostic>;
|
||||
|
||||
pub type DResult<T> = ::std::result::Result<T, Diagnostics>;
|
||||
|
||||
// An experiment.
|
||||
pub struct Diagnostics(Vec<Diagnostic>);
|
||||
|
||||
impl Diagnostics {
|
||||
pub fn new() -> Self {
|
||||
Diagnostics(vec![])
|
||||
}
|
||||
|
||||
pub fn push(&mut self, diag: Diagnostic) {
|
||||
self.0.push(diag);
|
||||
}
|
||||
|
||||
pub fn join(mut self, mut diags: Diagnostics) -> Self {
|
||||
self.0.append(&mut diags.0);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn emit_head(self) -> Diagnostic {
|
||||
let mut iter = self.0.into_iter();
|
||||
let mut last = iter.next().expect("Diagnostic::emit_head empty");
|
||||
for diag in iter {
|
||||
last.emit();
|
||||
last = diag;
|
||||
}
|
||||
|
||||
last
|
||||
}
|
||||
|
||||
pub fn head_err_or<T>(self, ok: T) -> PResult<T> {
|
||||
match self.0.is_empty() {
|
||||
true => Ok(ok),
|
||||
false => Err(self.emit_head())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn err_or<T>(self, ok: T) -> DResult<T> {
|
||||
match self.0.is_empty() {
|
||||
true => Ok(ok),
|
||||
false => Err(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Diagnostic> for Diagnostics {
|
||||
fn from(diag: Diagnostic) -> Self {
|
||||
Diagnostics(vec![diag])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Diagnostic>> for Diagnostics {
|
||||
fn from(diags: Vec<Diagnostic>) -> Self {
|
||||
Diagnostics(diags)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SpanExt {
|
||||
fn trimmed(&self, left: usize, right: usize) -> Option<Span>;
|
||||
}
|
||||
|
||||
impl SpanExt for Span {
|
||||
/// Trim the span on the left by `left` characters and on the right by
|
||||
/// `right` characters.
|
||||
fn trimmed(&self, left: usize, right: usize) -> Option<Span> {
|
||||
let inner: InnerSpan = unsafe { ::std::mem::transmute(*self) };
|
||||
if left > u32::max_value() as usize || right > u32::max_value() as usize {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ensure that the addition won't overflow.
|
||||
let (left, right) = (left as u32, right as u32);
|
||||
if u32::max_value() - left < inner.lo().to_u32() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ensure that the subtraction won't underflow.
|
||||
if right > inner.hi().to_u32() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_lo = inner.lo() + BytePos(left);
|
||||
let new_hi = inner.hi() - BytePos(right);
|
||||
|
||||
// Ensure we're still inside the old `Span` and didn't cross paths.
|
||||
if new_lo >= new_hi {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_inner = inner.with_lo(new_lo).with_hi(new_hi);
|
||||
Some(unsafe { ::std::mem::transmute(new_inner) })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
// #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
// #[macro_use] extern crate rocket;
|
||||
|
||||
// #[get("/", rank = 1)]
|
||||
// fn get1() -> &'static str { "hi" }
|
||||
|
||||
// #[get("/", rank = 2)]
|
||||
// fn get2() -> &'static str { "hi" }
|
||||
|
||||
// #[get("/", rank = 3)]
|
||||
// fn get3() -> &'static str { "hi" }
|
||||
|
||||
// #[get("/")] fn get() { }
|
||||
// #[route(GET, "/")] fn get_r() { }
|
||||
|
||||
// #[put("/")] fn put() { }
|
||||
// #[route(PUT, "/")] fn put_r() { }
|
||||
|
||||
// #[post("/")] fn post() { }
|
||||
// #[route(POST, "/")] fn post_r() { }
|
||||
|
||||
// #[delete("/")] fn delete() { }
|
||||
// #[route(DELETE, "/")] fn delete_r() { }
|
||||
|
||||
// #[head("/")] fn head() { }
|
||||
// #[route(HEAD, "/")] fn head_r() { }
|
||||
|
||||
// #[patch("/")] fn patch() { }
|
||||
// #[route(PATCH, "/")] fn patch_r() { }
|
||||
|
||||
// #[options("/")] fn options() { }
|
||||
// #[route(OPTIONS, "/")] fn options_r() { }
|
||||
|
||||
// use rocket::http::{Cookies, RawStr};
|
||||
// use rocket::request::Form;
|
||||
|
||||
// #[derive(FromForm)]
|
||||
// struct User<'a> {
|
||||
// name: &'a RawStr,
|
||||
// nickname: String,
|
||||
// }
|
||||
|
||||
// #[post("/<_name>?<_query>", format = "application/json", data = "<user>", rank = 2)]
|
||||
// fn get(
|
||||
// _name: &RawStr,
|
||||
// _query: User,
|
||||
// user: Form<User>,
|
||||
// _cookies: Cookies
|
||||
// ) -> String {
|
||||
// format!("{}:{}", user.name, user.nickname)
|
||||
// }
|
||||
|
||||
// #[post("/", format = "application/x-custom")]
|
||||
// fn get() -> &'static str { "hi" }
|
||||
|
||||
// #[get("/test/<one>/<two>/<three>")]
|
||||
// fn get(one: String, two: usize, three: isize) -> &'static str { "hi" }
|
||||
|
||||
// #[get("/test/<_one>/<_two>/<__three>")]
|
||||
// fn ignored(_one: String, _two: usize, __three: isize) -> &'static str { "hi" }
|
||||
|
||||
// #[get("/")]
|
||||
// fn get() -> &'static str { "hi" }
|
||||
|
||||
// #[get("/")]
|
||||
// fn get_empty() { }
|
||||
|
||||
// #[get("/one")]
|
||||
// fn one() { }
|
||||
|
||||
// #[get("/two")]
|
||||
// fn two() { }
|
||||
|
||||
// #[get("/three")]
|
||||
// fn three() { }
|
||||
|
||||
// #[get("/four")]
|
||||
// fn four() { }
|
||||
|
||||
// #[test]
|
||||
// fn main() {
|
||||
// let instance = rocket::ignite()
|
||||
// .mount("/", routes![one]);
|
||||
|
||||
// let other = instance.mount("/", routes![two]);
|
||||
// other.mount("/", routes![three])
|
||||
// .mount("/", routes![four]);
|
||||
|
||||
// rocket::ignite()
|
||||
// .mount("/", routes![one])
|
||||
// .mount("/", routes![two])
|
||||
// .mount("/", routes![three])
|
||||
// .mount("/", routes![four]);
|
||||
|
||||
// let a = rocket::ignite()
|
||||
// .mount("/", routes![one])
|
||||
// .mount("/", routes![two]);
|
||||
|
||||
// let b = a.mount("/", routes![three])
|
||||
// .mount("/", routes![four]);
|
||||
// }
|
||||
|
||||
// #[get("/<todo>")]
|
||||
// fn todo(todo: String) -> String {
|
||||
// todo
|
||||
// }
|
||||
|
||||
// #[post("/<a>/<b..>")]
|
||||
// fn get(a: String, b: PathBuf) -> String {
|
||||
// format!("{}/{}", a, b.to_string_lossy())
|
||||
// }
|
||||
|
||||
// #[post("/<a>/<b..>")]
|
||||
// fn get2(a: String, b: Result<PathBuf, SegmentError>) -> String {
|
||||
// format!("{}/{}", a, b.unwrap().to_string_lossy())
|
||||
// }
|
||||
|
||||
|
||||
// #[test]
|
||||
// fn main() {
|
||||
// let _ = routes![todo];
|
||||
// }
|
|
@ -0,0 +1,58 @@
|
|||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
use rocket::{Request, Data, Outcome::*};
|
||||
use rocket::local::Client;
|
||||
use rocket::request::Form;
|
||||
use rocket::data::{self, FromDataSimple};
|
||||
use rocket::http::{RawStr, ContentType, Status};
|
||||
|
||||
// Test that the data parameters works as expected.
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct Inner<'r> {
|
||||
field: &'r RawStr
|
||||
}
|
||||
|
||||
struct Simple(String);
|
||||
|
||||
impl FromDataSimple for Simple {
|
||||
type Error = ();
|
||||
|
||||
fn from_data(_: &Request, data: Data) -> data::Outcome<Self, ()> {
|
||||
let mut string = String::new();
|
||||
if let Err(_) = data.open().take(64).read_to_string(&mut string) {
|
||||
return Failure((Status::InternalServerError, ()));
|
||||
}
|
||||
|
||||
Success(Simple(string))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/f", data = "<form>")]
|
||||
fn form(form: Form<Inner>) -> String { form.field.url_decode_lossy() }
|
||||
|
||||
#[post("/s", data = "<simple>")]
|
||||
fn simple(simple: Simple) -> String { simple.0 }
|
||||
|
||||
#[test]
|
||||
fn test_data() {
|
||||
let rocket = rocket::ignite().mount("/", routes![form, simple]);
|
||||
let client = Client::new(rocket).unwrap();
|
||||
|
||||
let mut response = client.post("/f")
|
||||
.header(ContentType::Form)
|
||||
.body("field=this%20is%20here")
|
||||
.dispatch();
|
||||
|
||||
assert_eq!(response.body_string().unwrap(), "this is here");
|
||||
|
||||
let mut response = client.post("/s").body("this is here").dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "this is here");
|
||||
|
||||
let mut response = client.post("/s").body("this%20is%20here").dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "this%20is%20here");
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::local::Client;
|
||||
use rocket::http::{ContentType, MediaType, Accept, Status};
|
||||
|
||||
// Test that known formats work as expected, including not colliding.
|
||||
|
||||
#[post("/", format = "json")]
|
||||
fn json() -> &'static str { "json" }
|
||||
|
||||
#[post("/", format = "xml")]
|
||||
fn xml() -> &'static str { "xml" }
|
||||
|
||||
// Unreachable. Written for codegen.
|
||||
#[post("/", format = "application/json", rank = 2)]
|
||||
fn json_long() -> &'static str { "json_long" }
|
||||
|
||||
#[post("/", format = "application/msgpack")]
|
||||
fn msgpack_long() -> &'static str { "msgpack_long" }
|
||||
|
||||
// Unreachable. Written for codegen.
|
||||
#[post("/", format = "msgpack", rank = 2)]
|
||||
fn msgpack() -> &'static str { "msgpack" }
|
||||
|
||||
#[get("/", format = "plain")]
|
||||
fn plain() -> &'static str { "plain" }
|
||||
|
||||
#[get("/", format = "binary")]
|
||||
fn binary() -> &'static str { "binary" }
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
fn other() -> &'static str { "other" }
|
||||
|
||||
#[test]
|
||||
fn test_formats() {
|
||||
let rocket = rocket::ignite()
|
||||
.mount("/", routes![json, xml, json_long, msgpack_long, msgpack,
|
||||
plain, binary, other]);
|
||||
|
||||
let client = Client::new(rocket).unwrap();
|
||||
|
||||
let mut response = client.post("/").header(ContentType::JSON).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "json");
|
||||
|
||||
let mut response = client.post("/").header(ContentType::MsgPack).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "msgpack_long");
|
||||
|
||||
let mut response = client.post("/").header(ContentType::XML).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "xml");
|
||||
|
||||
let mut response = client.get("/").header(Accept::Plain).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "plain");
|
||||
|
||||
let mut response = client.get("/").header(Accept::Binary).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "binary");
|
||||
|
||||
let mut response = client.get("/").header(ContentType::JSON).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "other");
|
||||
|
||||
let mut response = client.get("/").dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "other");
|
||||
}
|
||||
|
||||
// Test custom formats.
|
||||
|
||||
#[get("/", format = "application/foo")]
|
||||
fn get_foo() -> &'static str { "get_foo" }
|
||||
|
||||
#[post("/", format = "application/foo")]
|
||||
fn post_foo() -> &'static str { "post_foo" }
|
||||
|
||||
#[get("/", format = "bar/baz")]
|
||||
fn get_bar_baz() -> &'static str { "get_bar_baz" }
|
||||
|
||||
#[put("/", format = "bar/baz")]
|
||||
fn put_bar_baz() -> &'static str { "put_bar_baz" }
|
||||
|
||||
#[test]
|
||||
fn test_custom_formats() {
|
||||
let rocket = rocket::ignite()
|
||||
.mount("/", routes![get_foo, post_foo, get_bar_baz, put_bar_baz]);
|
||||
|
||||
let client = Client::new(rocket).unwrap();
|
||||
|
||||
let foo_a = Accept::new(&[MediaType::new("application", "foo").into()]);
|
||||
let foo_ct = ContentType::new("application", "foo");
|
||||
let bar_baz_ct = ContentType::new("bar", "baz");
|
||||
let bar_baz_a = Accept::new(&[MediaType::new("bar", "baz").into()]);
|
||||
|
||||
let mut response = client.get("/").header(foo_a).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "get_foo");
|
||||
|
||||
let mut response = client.post("/").header(foo_ct).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "post_foo");
|
||||
|
||||
let mut response = client.get("/").header(bar_baz_a).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "get_bar_baz");
|
||||
|
||||
let mut response = client.put("/").header(bar_baz_ct).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "put_bar_baz");
|
||||
|
||||
let response = client.get("/").dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// #[post("/<_name>?<_query>", format = "application/json", data = "<user>", rank = 2)]
|
||||
// fn get(
|
||||
// _name: &RawStr,
|
||||
// _query: User,
|
||||
// user: Form<User>,
|
||||
// _cookies: Cookies
|
||||
// ) -> String {
|
||||
// format!("{}:{}", user.name, user.nickname)
|
||||
// }
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::local::Client;
|
||||
|
||||
// Test that manual/auto ranking works as expected.
|
||||
|
||||
#[get("/<_number>")]
|
||||
fn get0(_number: u8) -> &'static str { "0" }
|
||||
|
||||
#[get("/<_number>", rank = 1)]
|
||||
fn get1(_number: u16) -> &'static str { "1" }
|
||||
|
||||
#[get("/<_number>", rank = 2)]
|
||||
fn get2(_number: u32) -> &'static str { "2" }
|
||||
|
||||
#[get("/<_number>", rank = 3)]
|
||||
fn get3(_number: u64) -> &'static str { "3" }
|
||||
|
||||
#[test]
|
||||
fn test_ranking() {
|
||||
let rocket = rocket::ignite().mount("/", routes![get0, get1, get2, get3]);
|
||||
let client = Client::new(rocket).unwrap();
|
||||
|
||||
let mut response = client.get("/0").dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "0");
|
||||
|
||||
let mut response = client.get(format!("/{}", 1 << 8)).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "1");
|
||||
|
||||
let mut response = client.get(format!("/{}", 1 << 16)).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "2");
|
||||
|
||||
let mut response = client.get(format!("/{}", 1u64 << 32)).dispatch();
|
||||
assert_eq!(response.body_string().unwrap(), "3");
|
||||
}
|
||||
|
||||
// Test a collision due to same auto rank.
|
||||
|
||||
#[get("/<_n>")]
|
||||
fn get0b(_n: u8) { }
|
||||
|
||||
#[test]
|
||||
fn test_rank_collision() {
|
||||
use rocket::error::LaunchErrorKind;
|
||||
|
||||
let rocket = rocket::ignite().mount("/", routes![get0, get0b]);
|
||||
let client_result = Client::new(rocket);
|
||||
match client_result.as_ref().map_err(|e| e.kind()) {
|
||||
Err(LaunchErrorKind::Collision(..)) => { /* o.k. */ },
|
||||
Ok(_) => panic!("client succeeded unexpectedly"),
|
||||
Err(e) => panic!("expected collision, got {}", e)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
#![feature(plugin, proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rocket::{Request, Outcome::*};
|
||||
use rocket::local::Client;
|
||||
use rocket::data::{self, Data, FromDataSimple};
|
||||
use rocket::request::Form;
|
||||
use rocket::http::{Status, RawStr, ContentType, uri::UriDisplay};
|
||||
|
||||
// Use all of the code generation avaiable at once.
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct Inner<'r> {
|
||||
field: &'r RawStr
|
||||
}
|
||||
|
||||
// TODO: Make this deriveable.
|
||||
impl<'a> UriDisplay for Inner<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "field={}", self.field)
|
||||
}
|
||||
}
|
||||
|
||||
struct Simple(String);
|
||||
|
||||
impl FromDataSimple for Simple {
|
||||
type Error = ();
|
||||
|
||||
fn from_data(_: &Request, data: Data) -> data::Outcome<Self, ()> {
|
||||
use std::io::Read;
|
||||
let mut string = String::new();
|
||||
data.open().take(64).read_to_string(&mut string).unwrap();
|
||||
Success(Simple(string))
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/<a>/<name>/name/<path..>?sky=blue&<sky>&<query..>", format = "json", data = "<simple>", rank = 138)]
|
||||
fn post1(
|
||||
sky: usize,
|
||||
name: &RawStr,
|
||||
a: String,
|
||||
query: Form<Inner>,
|
||||
path: PathBuf,
|
||||
simple: Simple,
|
||||
) -> String {
|
||||
let string = format!("{}, {}, {}, {}, {}, {}",
|
||||
sky, name, a, query.field, path.display(), simple.0);
|
||||
|
||||
let uri = uri!(post2: a, name.url_decode_lossy(), path, sky, query.into_inner());
|
||||
|
||||
format!("({}) ({})", string, uri.to_string())
|
||||
}
|
||||
|
||||
#[route(POST, path = "/<a>/<name>/name/<path..>?sky=blue&<sky>&<query..>", format = "json", data = "<simple>", rank = 138)]
|
||||
fn post2(
|
||||
sky: usize,
|
||||
name: &RawStr,
|
||||
a: String,
|
||||
query: Form<Inner>,
|
||||
path: PathBuf,
|
||||
simple: Simple,
|
||||
) -> String {
|
||||
let string = format!("{}, {}, {}, {}, {}, {}",
|
||||
sky, name, a, query.field, path.display(), simple.0);
|
||||
|
||||
let uri = uri!(post2: a, name.url_decode_lossy(), path, sky, query.into_inner());
|
||||
|
||||
format!("({}) ({})", string, uri.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_route() {
|
||||
let rocket = rocket::ignite()
|
||||
.mount("/1", routes![post1])
|
||||
.mount("/2", routes![post2]);
|
||||
|
||||
let client = Client::new(rocket).unwrap();
|
||||
|
||||
let a = "A%20A";
|
||||
let name = "Bob%20McDonald";
|
||||
let path = "this/path/here";
|
||||
let sky = 777;
|
||||
let query = "field=inside";
|
||||
let simple = "data internals";
|
||||
|
||||
let path_part = format!("/{}/{}/name/{}", a, name, path);
|
||||
let query_part = format!("?sky={}&sky=blue&{}", sky, query);
|
||||
let uri = format!("{}{}", path_part, query_part);
|
||||
let expected_uri = format!("{}?sky=blue&sky={}&{}", path_part, sky, query);
|
||||
|
||||
let response = client.post(&uri).body(simple).dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
|
||||
let response = client.post(format!("/1{}", uri)).body(simple).dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
|
||||
let mut response = client
|
||||
.post(format!("/1{}", uri))
|
||||
.header(ContentType::JSON)
|
||||
.body(simple)
|
||||
.dispatch();
|
||||
|
||||
assert_eq!(response.body_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})",
|
||||
sky, name, "A A", "inside", path, simple, expected_uri));
|
||||
|
||||
let response = client.post(format!("/2{}", uri)).body(simple).dispatch();
|
||||
assert_eq!(response.status(), Status::NotFound);
|
||||
|
||||
let mut response = client
|
||||
.post(format!("/2{}", uri))
|
||||
.header(ContentType::JSON)
|
||||
.body(simple)
|
||||
.dispatch();
|
||||
|
||||
assert_eq!(response.body_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})",
|
||||
sky, name, "A A", "inside", path, simple, expected_uri));
|
||||
}
|
|
@ -16,11 +16,11 @@ const CATCH: &str = "Catcher";
|
|||
//~^ HELP #[catch(404)]
|
||||
fn e1(_request: &Request) { }
|
||||
|
||||
#[catch(code = "404")] //~ ERROR unexpected named parameter
|
||||
#[catch(code = "404")] //~ ERROR unexpected parameter
|
||||
//~^ HELP #[catch(404)]
|
||||
fn e2(_request: &Request) { }
|
||||
|
||||
#[catch(code = 404)] //~ ERROR unexpected named parameter
|
||||
#[catch(code = 404)] //~ ERROR unexpected parameter
|
||||
//~^ HELP #[catch(404)]
|
||||
fn e3(_request: &Request) { }
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ error: invalid value: expected unsigned integer literal
|
|||
|
|
||||
= help: `#[catch]` expects a single status integer, e.g.: #[catch(404)]
|
||||
|
||||
error: unexpected named parameter: expected bare literal
|
||||
error: unexpected parameter: expected literal or identifier
|
||||
--> $DIR/catch.rs:19:9
|
||||
|
|
||||
19 | #[catch(code = "404")] //~ ERROR unexpected named parameter
|
||||
|
@ -30,7 +30,7 @@ error: unexpected named parameter: expected bare literal
|
|||
|
|
||||
= help: `#[catch]` expects a single status integer, e.g.: #[catch(404)]
|
||||
|
||||
error: unexpected named parameter: expected bare literal
|
||||
error: unexpected parameter: expected literal or identifier
|
||||
--> $DIR/catch.rs:23:9
|
||||
|
|
||||
23 | #[catch(code = 404)] //~ ERROR unexpected named parameter
|
||||
|
|
|
@ -26,6 +26,7 @@ rustls = { version = "0.13", optional = true }
|
|||
state = "0.4"
|
||||
cookie = { version = "0.11", features = ["percent-encode", "secure"] }
|
||||
pear = { git = "http://github.com/SergioBenitez/Pear", rev = "b475140" }
|
||||
unicode-xid = "0.1"
|
||||
|
||||
[dependencies.hyper-sync-rustls]
|
||||
version = "=0.3.0-rc.3"
|
||||
|
|
|
@ -35,9 +35,8 @@ use Header;
|
|||
/// a handler to retrieve the value of a "message" cookie.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::http::Cookies;
|
||||
///
|
||||
/// #[get("/message")]
|
||||
|
@ -56,9 +55,8 @@ use Header;
|
|||
/// [private cookie]: /rocket/http/enum.Cookies.html#private-cookies
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, never_type)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro, never_type)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// use rocket::http::Status;
|
||||
/// use rocket::outcome::IntoOutcome;
|
||||
|
|
|
@ -21,6 +21,7 @@ extern crate cookie;
|
|||
extern crate time;
|
||||
extern crate indexmap;
|
||||
extern crate state;
|
||||
extern crate unicode_xid;
|
||||
|
||||
pub mod hyper;
|
||||
pub mod uri;
|
||||
|
@ -30,6 +31,9 @@ pub mod ext;
|
|||
#[cfg(feature = "tls")]
|
||||
pub mod tls;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod route;
|
||||
|
||||
#[macro_use]
|
||||
mod docify;
|
||||
#[macro_use]
|
||||
|
@ -50,18 +54,19 @@ pub mod uncased;
|
|||
// We need to export these for codegen, but otherwise it's unnecessary.
|
||||
// TODO: Expose a `const fn` from ContentType when possible. (see RFC#1817)
|
||||
// FIXME(rustc): These show up in the rexported module.
|
||||
#[doc(hidden)] pub use self::parse::Indexed;
|
||||
#[doc(hidden)] pub use self::media_type::{MediaParams, Source};
|
||||
#[doc(hidden)] pub use parse::Indexed;
|
||||
#[doc(hidden)] pub use media_type::{MediaParams, Source};
|
||||
#[doc(hidden)] pub use smallvec::{SmallVec, Array};
|
||||
|
||||
// This one we need to expose for core.
|
||||
#[doc(hidden)] pub use self::cookies::{Key, CookieJar};
|
||||
#[doc(hidden)] pub use cookies::{Key, CookieJar};
|
||||
|
||||
pub use self::method::Method;
|
||||
pub use self::content_type::ContentType;
|
||||
pub use self::accept::{Accept, QMediaType};
|
||||
pub use self::status::{Status, StatusClass};
|
||||
pub use self::header::{Header, HeaderMap};
|
||||
pub use self::raw_str::RawStr;
|
||||
pub use method::Method;
|
||||
pub use content_type::ContentType;
|
||||
pub use accept::{Accept, QMediaType};
|
||||
pub use status::{Status, StatusClass};
|
||||
pub use header::{Header, HeaderMap};
|
||||
pub use raw_str::RawStr;
|
||||
|
||||
pub use self::media_type::MediaType;
|
||||
pub use self::cookies::{Cookie, SameSite, Cookies};
|
||||
pub use media_type::MediaType;
|
||||
pub use cookies::{Cookie, SameSite, Cookies};
|
||||
|
|
|
@ -45,6 +45,16 @@ impl<'a, T: ?Sized + ToOwned + 'a, C: Into<Cow<'a, T>>> From<C> for Indexed<'a,
|
|||
}
|
||||
|
||||
impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> {
|
||||
/// Panics if `self` is not an `Indexed`.
|
||||
#[inline(always)]
|
||||
pub fn indices(self) -> (usize, usize) {
|
||||
match self {
|
||||
Indexed::Indexed(a, b) => (a, b),
|
||||
_ => panic!("cannot convert indexed T to U unless indexed")
|
||||
}
|
||||
}
|
||||
|
||||
/// Panics if `self` is not an `Indexed`.
|
||||
#[inline(always)]
|
||||
pub fn coerce<U: ?Sized + ToOwned>(self) -> Indexed<'a, U> {
|
||||
match self {
|
||||
|
@ -53,6 +63,7 @@ impl<'a, T: ?Sized + ToOwned + 'a> Indexed<'a, T> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Panics if `self` is not an `Indexed`.
|
||||
#[inline(always)]
|
||||
pub fn coerce_lifetime<'b>(self) -> Indexed<'b, T> {
|
||||
match self {
|
||||
|
|
|
@ -39,6 +39,22 @@ impl<'a> Error<'a> {
|
|||
|
||||
Error { expected: new_expected, context: pear_error.context }
|
||||
}
|
||||
|
||||
/// Returns the byte index into the text where the error occurred if it is
|
||||
/// known.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate rocket;
|
||||
/// use rocket::http::uri::Origin;
|
||||
///
|
||||
/// let err = Origin::parse("/foo bar").unwrap_err();
|
||||
/// assert_eq!(err.index(), Some(4));
|
||||
/// ```
|
||||
pub fn index(&self) -> Option<usize> {
|
||||
self.context.as_ref().map(|c| c.offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Or<char, u8> {
|
||||
|
|
|
@ -44,7 +44,7 @@ use uncased::UncasedStr;
|
|||
///
|
||||
/// [`FromParam`]: /rocket/request/trait.FromParam.html
|
||||
/// [`FromFormValue`]: /rocket/request/trait.FromFormValue.html
|
||||
#[repr(C)]
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct RawStr(str);
|
||||
|
||||
|
@ -154,12 +154,51 @@ impl RawStr {
|
|||
/// assert_eq!(decoded, Ok("Hello, world!".to_string()));
|
||||
/// ```
|
||||
pub fn url_decode(&self) -> Result<String, Utf8Error> {
|
||||
// TODO: Make this more efficient!
|
||||
let replaced = self.replace("+", " ");
|
||||
RawStr::from_str(replaced.as_str())
|
||||
.percent_decode()
|
||||
.map(|cow| cow.into_owned())
|
||||
}
|
||||
|
||||
/// Returns a URL-decoded version of the string.
|
||||
///
|
||||
/// Any invalid UTF-8 percent-encoded byte sequences will be replaced <20>
|
||||
/// U+FFFD, the replacement character. This is identical to lossy percent
|
||||
/// decoding except that `+` characters are converted into spaces. This is
|
||||
/// the encoding used by form values.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// With a valid string:
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate rocket;
|
||||
/// use rocket::http::RawStr;
|
||||
///
|
||||
/// let raw_str: &RawStr = "Hello%2C+world%21".into();
|
||||
/// let decoded = raw_str.url_decode_lossy();
|
||||
/// assert_eq!(decoded, "Hello, world!");
|
||||
/// ```
|
||||
///
|
||||
/// With an invalid string:
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate rocket;
|
||||
/// use rocket::http::RawStr;
|
||||
///
|
||||
/// // Note: Rocket should never hand you a bad `&RawStr`.
|
||||
/// let bad_str = unsafe { ::std::str::from_utf8_unchecked(b"a+b=\xff") };
|
||||
/// let bad_raw_str = RawStr::from_str(bad_str);
|
||||
/// assert_eq!(bad_raw_str.url_decode_lossy(), "a b=<3D>");
|
||||
/// ```
|
||||
pub fn url_decode_lossy(&self) -> String {
|
||||
let replaced = self.replace("+", " ");
|
||||
RawStr::from_str(replaced.as_str())
|
||||
.percent_decode_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Returns an HTML escaped version of `self`. Allocates only when
|
||||
/// characters need to be escaped.
|
||||
///
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
use std::borrow::Cow;
|
||||
use unicode_xid::UnicodeXID;
|
||||
|
||||
use ext::IntoOwned;
|
||||
use uri::{Uri, Origin};
|
||||
|
||||
use self::Error::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum Kind {
|
||||
Static,
|
||||
Single,
|
||||
Multi,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum Source {
|
||||
Path,
|
||||
Query,
|
||||
Data,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RouteSegment<'a> {
|
||||
pub string: Cow<'a, str>,
|
||||
pub kind: Kind,
|
||||
pub source: Source,
|
||||
pub name: Cow<'a, str>,
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> IntoOwned for RouteSegment<'a> {
|
||||
type Owned = RouteSegment<'static>;
|
||||
|
||||
#[inline]
|
||||
fn into_owned(self) -> Self::Owned {
|
||||
RouteSegment {
|
||||
string: IntoOwned::into_owned(self.string),
|
||||
kind: self.kind,
|
||||
source: self.source,
|
||||
name: IntoOwned::into_owned(self.name),
|
||||
index: self.index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum Error<'a> {
|
||||
Empty,
|
||||
Ident(&'a str),
|
||||
Ignored,
|
||||
MissingClose,
|
||||
Malformed,
|
||||
Uri,
|
||||
Trailing(&'a str)
|
||||
}
|
||||
|
||||
pub type SResult<'a> = Result<RouteSegment<'a>, (&'a str, Error<'a>)>;
|
||||
|
||||
#[inline]
|
||||
fn is_ident_start(c: char) -> bool {
|
||||
('a' <= c && c <= 'z')
|
||||
|| ('A' <= c && c <= 'Z')
|
||||
|| c == '_'
|
||||
|| (c > '\x7f' && UnicodeXID::is_xid_start(c))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_ident_continue(c: char) -> bool {
|
||||
('a' <= c && c <= 'z')
|
||||
|| ('A' <= c && c <= 'Z')
|
||||
|| c == '_'
|
||||
|| ('0' <= c && c <= '9')
|
||||
|| (c > '\x7f' && UnicodeXID::is_xid_continue(c))
|
||||
}
|
||||
|
||||
fn is_valid_ident(string: &str) -> bool {
|
||||
let mut chars = string.chars();
|
||||
match chars.next() {
|
||||
Some(c) => is_ident_start(c) && chars.all(is_ident_continue),
|
||||
None => false
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RouteSegment<'a> {
|
||||
pub fn parse_one(segment: &str) -> Result<RouteSegment, Error> {
|
||||
let (string, source, index) = (segment.into(), Source::Unknown, None);
|
||||
|
||||
// Check if this is a dynamic param. If so, check its well-formedness.
|
||||
if segment.starts_with('<') && segment.ends_with('>') {
|
||||
let mut kind = Kind::Single;
|
||||
let mut name = &segment[1..(segment.len() - 1)];
|
||||
if name.ends_with("..") {
|
||||
kind = Kind::Multi;
|
||||
name = &name[..(name.len() - 2)];
|
||||
}
|
||||
|
||||
if name.is_empty() {
|
||||
return Err(Empty);
|
||||
} else if !is_valid_ident(name) {
|
||||
return Err(Ident(name));
|
||||
} else if name == "_" {
|
||||
return Err(Ignored);
|
||||
}
|
||||
|
||||
let name = name.into();
|
||||
return Ok(RouteSegment { string, source, name, kind, index });
|
||||
} else if segment.is_empty() {
|
||||
return Err(Empty);
|
||||
} else if segment.starts_with('<') && segment.len() > 1 {
|
||||
return Err(MissingClose);
|
||||
} else if segment.contains('>') || segment.contains('<') {
|
||||
return Err(Malformed);
|
||||
} else if Uri::percent_encode(segment) != segment
|
||||
|| Uri::percent_decode_lossy(segment.as_bytes()) != segment
|
||||
|| segment.contains('+') {
|
||||
return Err(Uri);
|
||||
}
|
||||
|
||||
Ok(RouteSegment {
|
||||
string, source, index,
|
||||
name: segment.into(),
|
||||
kind: Kind::Static,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_many(
|
||||
string: &str,
|
||||
sep: char,
|
||||
source: Source,
|
||||
) -> impl Iterator<Item = SResult> {
|
||||
let mut last_multi_seg: Option<&str> = None;
|
||||
string.split(sep).filter(|s| !s.is_empty()).enumerate().map(move |(i, seg)| {
|
||||
if let Some(multi_seg) = last_multi_seg {
|
||||
return Err((seg, Trailing(multi_seg)));
|
||||
}
|
||||
|
||||
let mut parsed = Self::parse_one(seg).map_err(|e| (seg, e))?;
|
||||
if parsed.kind == Kind::Multi {
|
||||
last_multi_seg = Some(seg);
|
||||
}
|
||||
|
||||
parsed.index = Some(i);
|
||||
parsed.source = source;
|
||||
Ok(parsed)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_path(uri: &'a Origin) -> impl Iterator<Item = SResult<'a>> {
|
||||
Self::parse_many(uri.path(), '/', Source::Path)
|
||||
}
|
||||
|
||||
pub fn parse_query(uri: &'a Origin) -> Option<impl Iterator<Item = SResult<'a>>> {
|
||||
uri.query().map(|q| Self::parse_many(q, '&', Source::Query))
|
||||
}
|
||||
}
|
|
@ -98,7 +98,7 @@ use {RawStr, uri::UriDisplay};
|
|||
///
|
||||
/// [`uri!`]: /rocket_codegen/#typed-uris-uri
|
||||
/// [`UriDisplay`]: /rocket/http/uri/trait.UriDisplay.html
|
||||
pub trait FromUriParam<T>: UriDisplay {
|
||||
pub trait FromUriParam<T> {
|
||||
/// The resulting type of this conversion.
|
||||
type Target: UriDisplay;
|
||||
|
||||
|
@ -141,6 +141,20 @@ impl<'a, 'b> FromUriParam<&'a str> for &'b RawStr {
|
|||
fn from_uri_param(param: &'a str) -> &'a str { param }
|
||||
}
|
||||
|
||||
/// A no cost conversion allowing a `String` to be used in place of an `&RawStr`.
|
||||
impl<'a> FromUriParam<String> for &'a RawStr {
|
||||
type Target = String;
|
||||
#[inline(always)]
|
||||
fn from_uri_param(param: String) -> String { param }
|
||||
}
|
||||
|
||||
/// A no cost conversion allowing a `String` to be used in place of an `&str`.
|
||||
impl<'a> FromUriParam<String> for &'a str {
|
||||
type Target = String;
|
||||
#[inline(always)]
|
||||
fn from_uri_param(param: String) -> String { param }
|
||||
}
|
||||
|
||||
/// A no cost conversion allowing an `&Path` to be used in place of a `PathBuf`.
|
||||
impl<'a> FromUriParam<&'a Path> for PathBuf {
|
||||
type Target = &'a Path;
|
||||
|
@ -151,6 +165,7 @@ impl<'a> FromUriParam<&'a Path> for PathBuf {
|
|||
/// A no cost conversion allowing an `&str` to be used in place of a `PathBuf`.
|
||||
impl<'a> FromUriParam<&'a str> for PathBuf {
|
||||
type Target = &'a Path;
|
||||
|
||||
#[inline(always)]
|
||||
fn from_uri_param(param: &'a str) -> &'a Path {
|
||||
Path::new(param)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#![feature(test, plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
extern crate rocket;
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::config::{Environment, Config, LoggingLevel};
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#![feature(test, plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
extern crate rocket;
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::config::{Environment, Config, LoggingLevel};
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
#![feature(test, plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
// #![feature(alloc_system)]
|
||||
// extern crate alloc_system;
|
||||
|
||||
extern crate rocket;
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::config::{Environment, Config, LoggingLevel};
|
||||
use rocket::http::RawStr;
|
||||
|
|
|
@ -33,8 +33,7 @@ use yansi::Color::*;
|
|||
/// declared using the `catch` decorator, as follows:
|
||||
///
|
||||
/// ```rust
|
||||
/// #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
/// #![plugin(rocket_codegen)]
|
||||
/// #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
///
|
||||
/// #[macro_use] extern crate rocket;
|
||||
///
|
||||
|
|
|
@ -31,8 +31,7 @@ const PEEK_BYTES: usize = 512;
|
|||
/// route parameter as follows:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # type DataGuard = ::rocket::data::Data;
|
||||
/// #[post("/submit", data = "<var>")]
|
||||
|
|
|
@ -130,8 +130,7 @@ pub type Transformed<'a, T> =
|
|||
/// if the guard returns successfully.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # type DataGuard = ::rocket::data::Data;
|
||||
/// #[post("/submit", data = "<var>")]
|
||||
|
@ -177,9 +176,8 @@ pub type Transformed<'a, T> =
|
|||
/// `String` (an `&str`).
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # #[derive(Debug)]
|
||||
/// # struct Name<'a> { first: &'a str, last: &'a str, }
|
||||
/// use std::io::{self, Read};
|
||||
|
@ -424,8 +422,7 @@ impl<'f> FromData<'f> for Data {
|
|||
/// that you can retrieve it directly from a client's request body:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # type Person = ::rocket::data::Data;
|
||||
/// #[post("/person", data = "<person>")]
|
||||
|
@ -437,11 +434,8 @@ impl<'f> FromData<'f> for Data {
|
|||
/// A `FromDataSimple` implementation allowing this looks like:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![allow(unused_attributes)]
|
||||
/// # #![allow(unused_variables)]
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// # #[derive(Debug)]
|
||||
/// # struct Person { name: String, age: u16 }
|
||||
|
|
|
@ -86,8 +86,7 @@ pub type Outcome<'r> = outcome::Outcome<Response<'r>, Status, Data>;
|
|||
/// managed state and a static route, as follows:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// # #[derive(Copy, Clone)]
|
||||
|
|
|
@ -57,19 +57,20 @@
|
|||
//! Then, add the following to the top of your `main.rs` file:
|
||||
//!
|
||||
//! ```rust
|
||||
//! #![feature(plugin, decl_macro)]
|
||||
//! # #![allow(unused_attributes)]
|
||||
//! #![plugin(rocket_codegen)]
|
||||
//! #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
//!
|
||||
//! extern crate rocket;
|
||||
//! #[macro_use] extern crate rocket;
|
||||
//! #
|
||||
//! # #[get("/")]
|
||||
//! # fn hello() { }
|
||||
//! # fn main() { rocket::ignite().mount("/", routes![hello]); }
|
||||
//! ```
|
||||
//!
|
||||
//! See the [guide](https://rocket.rs/guide) for more information on how to
|
||||
//! write Rocket applications. Here's a simple example to get you started:
|
||||
//!
|
||||
//! ```rust
|
||||
//! #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
//! #![plugin(rocket_codegen)]
|
||||
//! #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
//!
|
||||
//! #[macro_use] extern crate rocket;
|
||||
//!
|
||||
|
|
|
@ -67,10 +67,9 @@
|
|||
//! consider the following complete "Hello, world!" application, with testing.
|
||||
//!
|
||||
//! ```rust
|
||||
//! #![feature(plugin, decl_macro)]
|
||||
//! #![plugin(rocket_codegen)]
|
||||
//! #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
//!
|
||||
//! extern crate rocket;
|
||||
//! #[macro_use] extern crate rocket;
|
||||
//!
|
||||
//! #[get("/")]
|
||||
//! fn hello() -> &'static str {
|
||||
|
|
|
@ -209,13 +209,15 @@ pub fn init(level: LoggingLevel) -> bool {
|
|||
try_init(level, true)
|
||||
}
|
||||
|
||||
// This method exists as a shim for the log macros that need to be called from
|
||||
// an end user's code. It was added as part of the work to support database
|
||||
// connection pools via procedural macros.
|
||||
#[doc(hidden)]
|
||||
pub fn log_err(indented: bool, msg: &str) {
|
||||
match indented {
|
||||
true => error_!("{}", msg),
|
||||
false => error!("{}", msg),
|
||||
}
|
||||
// Expose logging macros as (hidden) funcions for use by core/contrib codegen.
|
||||
macro_rules! external_log_function {
|
||||
($fn_name:ident: $macro_name:ident) => (
|
||||
#[doc(hidden)] #[inline(always)]
|
||||
pub fn $fn_name(msg: &str) { $macro_name!("{}", msg); }
|
||||
)
|
||||
}
|
||||
|
||||
external_log_function!(log_error: error);
|
||||
external_log_function!(log_error_: error_);
|
||||
external_log_function!(log_warn: warn);
|
||||
external_log_function!(log_warn_: warn_);
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::ops::Deref;
|
|||
use outcome::Outcome::*;
|
||||
use request::{Request, form::{FromForm, FormItems, FormDataError}};
|
||||
use data::{Outcome, Transform, Transformed, Data, FromData};
|
||||
use http::Status;
|
||||
use http::{Status, uri::FromUriParam};
|
||||
|
||||
/// A data guard for parsing [`FromForm`] types strictly.
|
||||
///
|
||||
|
@ -42,9 +42,8 @@ use http::Status;
|
|||
/// can access fields of `T` transparently through a `Form<T>`:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #![allow(deprecated, unused_attributes)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::request::Form;
|
||||
/// use rocket::http::RawStr;
|
||||
|
@ -72,9 +71,8 @@ use http::Status;
|
|||
/// A handler that handles a form of this type can similarly by written:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #![allow(deprecated, unused_attributes)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # use rocket::request::Form;
|
||||
/// # #[derive(FromForm)]
|
||||
|
@ -119,7 +117,7 @@ use http::Status;
|
|||
/// forms = 524288
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct Form<T>(T);
|
||||
pub struct Form<T>(crate T);
|
||||
|
||||
impl<T> Form<T> {
|
||||
/// Consumes `self` and returns the parsed value.
|
||||
|
@ -127,8 +125,7 @@ impl<T> Form<T> {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::request::Form;
|
||||
///
|
||||
|
@ -227,3 +224,12 @@ impl<'f, T: FromForm<'f>> FromData<'f> for Form<T> {
|
|||
<Form<T>>::from_data(o.borrowed()?, true).map(Form)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f, A, T: FromUriParam<A> + FromForm<'f>> FromUriParam<A> for Form<T> {
|
||||
type Target = T::Target;
|
||||
|
||||
#[inline(always)]
|
||||
fn from_uri_param(param: A) -> Self::Target {
|
||||
T::from_uri_param(param)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,24 +4,46 @@ use http::RawStr;
|
|||
|
||||
/// Iterator over the key/value pairs of a given HTTP form string.
|
||||
///
|
||||
/// **Note:** The returned key/value pairs are _not_ URL decoded. To URL decode
|
||||
/// the raw strings, use the
|
||||
/// [`url_decode`](/rocket/http/struct.RawStr.html#method.url_decode) method:
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::request::{FormItems, FromFormValue};
|
||||
///
|
||||
/// // Using the `key_value_decoded` method of `FormItem`.
|
||||
/// let form_string = "greeting=Hello%2C+Mark%21&username=jake%2Fother";
|
||||
/// for (key, value) in FormItems::from(form_string) {
|
||||
/// let decoded_value = value.url_decode();
|
||||
/// match key.as_str() {
|
||||
/// "greeting" => assert_eq!(decoded_value, Ok("Hello, Mark!".into())),
|
||||
/// "username" => assert_eq!(decoded_value, Ok("jake/other".into())),
|
||||
/// for (key, value) in FormItems::from(form_string).map(|i| i.key_value_decoded()) {
|
||||
/// match &*key {
|
||||
/// "greeting" => assert_eq!(value, "Hello, Mark!".to_string()),
|
||||
/// "username" => assert_eq!(value, "jake/other".to_string()),
|
||||
/// _ => unreachable!()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Accessing the fields of `FormItem` directly, including `raw`.
|
||||
/// for item in FormItems::from(form_string) {
|
||||
/// match item.key.as_str() {
|
||||
/// "greeting" => {
|
||||
/// assert_eq!(item.raw, "greeting=Hello%2C+Mark%21");
|
||||
/// assert_eq!(item.value, "Hello%2C+Mark%21");
|
||||
/// assert_eq!(item.value.url_decode(), Ok("Hello, Mark!".into()));
|
||||
/// }
|
||||
/// "username" => {
|
||||
/// assert_eq!(item.raw, "username=jake%2Fother");
|
||||
/// assert_eq!(item.value, "jake%2Fother");
|
||||
/// assert_eq!(item.value.url_decode(), Ok("jake/other".into()));
|
||||
/// }
|
||||
/// _ => unreachable!()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Form Items via. `FormItem`
|
||||
///
|
||||
/// This iterator returns values of the type [`FormItem`]. To access the
|
||||
/// associated key/value pairs of the form item, either directly access them via
|
||||
/// the [`key`](FormItem.key) and [`value`](FormItem.value) fields, use the
|
||||
/// [`FormItem::key_value()`] method to get a tuple of the _raw_ `(key, value)`,
|
||||
/// or use the [`FormItems::key_value_decoded()`] method to get a tuple of the
|
||||
/// decoded (`key`, `value`).
|
||||
///
|
||||
/// # Completion
|
||||
///
|
||||
/// The iterator keeps track of whether the form string was parsed to completion
|
||||
|
@ -52,7 +74,7 @@ use http::RawStr;
|
|||
///
|
||||
/// // prints "greeting = hello", "username = jake", and "done = "
|
||||
/// let form_string = "greeting=hello&username=jake&done";
|
||||
/// for (key, value) in FormItems::from(form_string) {
|
||||
/// for (key, value) in FormItems::from(form_string).map(|item| item.key_value()) {
|
||||
/// println!("{} = {}", key, value);
|
||||
/// }
|
||||
/// ```
|
||||
|
@ -66,23 +88,66 @@ use http::RawStr;
|
|||
/// let mut items = FormItems::from(form_string);
|
||||
///
|
||||
/// let next = items.next().unwrap();
|
||||
/// assert_eq!(next.0, "greeting");
|
||||
/// assert_eq!(next.1, "hello");
|
||||
/// assert_eq!(next.key, "greeting");
|
||||
/// assert_eq!(next.value, "hello");
|
||||
///
|
||||
/// let next = items.next().unwrap();
|
||||
/// assert_eq!(next.0, "username");
|
||||
/// assert_eq!(next.1, "jake");
|
||||
/// assert_eq!(next.key, "username");
|
||||
/// assert_eq!(next.value, "jake");
|
||||
///
|
||||
/// let next = items.next().unwrap();
|
||||
/// assert_eq!(next.0, "done");
|
||||
/// assert_eq!(next.1, "");
|
||||
/// assert_eq!(next.key, "done");
|
||||
/// assert_eq!(next.value, "");
|
||||
///
|
||||
/// assert_eq!(items.next(), None);
|
||||
/// assert!(items.completed());
|
||||
/// ```
|
||||
pub struct FormItems<'f> {
|
||||
#[derive(Debug)]
|
||||
pub enum FormItems<'f> {
|
||||
Raw {
|
||||
string: &'f RawStr,
|
||||
next_index: usize
|
||||
},
|
||||
Cooked {
|
||||
items: &'f [FormItem<'f>],
|
||||
next_index: usize
|
||||
}
|
||||
}
|
||||
|
||||
/// A form items returned by the [`FormItems`] iterator.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct FormItem<'f> {
|
||||
/// The full, nonempty string for the item, not including delimiters.
|
||||
pub raw: &'f RawStr,
|
||||
/// The key for the item, which may be empty if `value` is nonempty.
|
||||
///
|
||||
/// **Note:** The key is _not_ URL decoded. To URL decode the raw strings,
|
||||
/// use the [`RawStr::url_decode()`] method or access key-value pairs with
|
||||
/// [`FromItem::key_value_decoded()`].
|
||||
pub key: &'f RawStr,
|
||||
/// The value for the item, which may be empty if `key` is nonempty.
|
||||
///
|
||||
/// **Note:** The value is _not_ URL decoded. To URL decode the raw strings,
|
||||
/// use the [`RawStr::url_decode()`] method or access key-value pairs with
|
||||
/// [`FromItem::key_value_decoded()`].
|
||||
pub value: &'f RawStr
|
||||
}
|
||||
|
||||
impl<'f> FormItem<'f> {
|
||||
#[inline(always)]
|
||||
pub fn key_value(&self) -> (&'f RawStr, &'f RawStr) {
|
||||
(self.key, self.value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn key_value_decoded(&self) -> (String, String) {
|
||||
(self.key.url_decode_lossy(), self.value.url_decode_lossy())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn explode(&self) -> (&'f RawStr, &'f RawStr, &'f RawStr) {
|
||||
(self.raw, self.key, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f> FormItems<'f> {
|
||||
|
@ -117,7 +182,10 @@ impl<'f> FormItems<'f> {
|
|||
/// ```
|
||||
#[inline]
|
||||
pub fn completed(&self) -> bool {
|
||||
self.next_index >= self.string.len()
|
||||
match self {
|
||||
FormItems::Raw { string, next_index } => *next_index >= string.len(),
|
||||
FormItems::Cooked { items, next_index } => *next_index >= items.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses all remaining key/value pairs and returns `true` if parsing ran
|
||||
|
@ -161,61 +229,38 @@ impl<'f> FormItems<'f> {
|
|||
#[inline]
|
||||
#[doc(hidden)]
|
||||
pub fn mark_complete(&mut self) {
|
||||
self.next_index = self.string.len()
|
||||
match self {
|
||||
FormItems::Raw { string, ref mut next_index } => *next_index = string.len(),
|
||||
FormItems::Cooked { items, ref mut next_index } => *next_index = items.len(),
|
||||
}
|
||||
|
||||
/// Retrieves the original string being parsed by this iterator. The string
|
||||
/// returned by this method does not change, regardless of the status of the
|
||||
/// iterator.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::request::FormItems;
|
||||
///
|
||||
/// let form_string = "a=b&c=d";
|
||||
/// let mut items = FormItems::from(form_string);
|
||||
/// assert_eq!(items.inner_str(), form_string);
|
||||
///
|
||||
/// assert!(items.next().is_some());
|
||||
/// assert_eq!(items.inner_str(), form_string);
|
||||
///
|
||||
/// assert!(items.next().is_some());
|
||||
/// assert_eq!(items.inner_str(), form_string);
|
||||
///
|
||||
/// assert!(items.next().is_none());
|
||||
/// assert_eq!(items.inner_str(), form_string);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn inner_str(&self) -> &'f RawStr {
|
||||
self.string
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f> From<&'f RawStr> for FormItems<'f> {
|
||||
/// Returns an iterator over the key/value pairs in the
|
||||
/// `x-www-form-urlencoded` form `string`.
|
||||
#[inline(always)]
|
||||
fn from(string: &'f RawStr) -> FormItems<'f> {
|
||||
FormItems { string, next_index: 0 }
|
||||
FormItems::Raw { string, next_index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f> From<&'f str> for FormItems<'f> {
|
||||
/// Returns an iterator over the key/value pairs in the
|
||||
/// `x-www-form-urlencoded` form `string`.
|
||||
#[inline(always)]
|
||||
fn from(string: &'f str) -> FormItems<'f> {
|
||||
FormItems::from(RawStr::from_str(string))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f> Iterator for FormItems<'f> {
|
||||
type Item = (&'f RawStr, &'f RawStr);
|
||||
impl<'f> From<&'f [FormItem<'f>]> for FormItems<'f> {
|
||||
#[inline(always)]
|
||||
fn from(items: &'f [FormItem<'f>]) -> FormItems<'f> {
|
||||
FormItems::Cooked { items, next_index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
fn raw<'f>(string: &mut &'f RawStr, index: &mut usize) -> Option<FormItem<'f>> {
|
||||
loop {
|
||||
let s = &self.string[self.next_index..];
|
||||
let start = *index;
|
||||
let s = &string[start..];
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
@ -232,85 +277,147 @@ impl<'f> Iterator for FormItems<'f> {
|
|||
None => (rest, rest.len())
|
||||
};
|
||||
|
||||
self.next_index += key_consumed + val_consumed;
|
||||
*index += key_consumed + val_consumed;
|
||||
let raw = &string[start..(start + key_consumed + value.len())];
|
||||
match (key.is_empty(), value.is_empty()) {
|
||||
(true, true) => continue,
|
||||
_ => return Some((key.into(), value.into()))
|
||||
}
|
||||
_ => return Some(FormItem {
|
||||
raw: raw.into(),
|
||||
key: key.into(),
|
||||
value: value.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::FormItems;
|
||||
impl<'f> Iterator for FormItems<'f> {
|
||||
type Item = FormItem<'f>;
|
||||
|
||||
macro_rules! check_form {
|
||||
(@bad $string:expr) => (check_form($string, None));
|
||||
($string:expr, $expected:expr) => (check_form($string, Some($expected)));
|
||||
}
|
||||
|
||||
fn check_form(string: &str, expected: Option<&[(&str, &str)]>) {
|
||||
let mut items = FormItems::from(string);
|
||||
let results: Vec<_> = items.by_ref().collect();
|
||||
if let Some(expected) = expected {
|
||||
assert_eq!(expected.len(), results.len(),
|
||||
"expected {:?}, got {:?} for {:?}", expected, results, string);
|
||||
|
||||
for i in 0..results.len() {
|
||||
let (expected_key, actual_key) = (expected[i].0, results[i].0);
|
||||
let (expected_val, actual_val) = (expected[i].1, results[i].1);
|
||||
|
||||
assert!(actual_key == expected_key,
|
||||
"key [{}] mismatch for {}: expected {}, got {}",
|
||||
i, string, expected_key, actual_key);
|
||||
|
||||
assert!(actual_val == expected_val,
|
||||
"val [{}] mismatch for {}: expected {}, got {}",
|
||||
i, string, expected_val, actual_val);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self {
|
||||
FormItems::Raw { ref mut string, ref mut next_index } => {
|
||||
raw(string, next_index)
|
||||
}
|
||||
FormItems::Cooked { items, ref mut next_index } => {
|
||||
if *next_index < items.len() {
|
||||
let item = items[*next_index];
|
||||
*next_index += 1;
|
||||
Some(item)
|
||||
} else {
|
||||
assert!(!items.exhaust(), "{} unexpectedly parsed successfully", string);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_form_string() {
|
||||
check_form!("username=user&password=pass",
|
||||
&[("username", "user"), ("password", "pass")]);
|
||||
|
||||
check_form!("user=user&user=pass", &[("user", "user"), ("user", "pass")]);
|
||||
check_form!("user=&password=pass", &[("user", ""), ("password", "pass")]);
|
||||
check_form!("user&password=pass", &[("user", ""), ("password", "pass")]);
|
||||
check_form!("foo&bar", &[("foo", ""), ("bar", "")]);
|
||||
|
||||
check_form!("a=b", &[("a", "b")]);
|
||||
check_form!("value=Hello+World", &[("value", "Hello+World")]);
|
||||
|
||||
check_form!("user=", &[("user", "")]);
|
||||
check_form!("user=&", &[("user", "")]);
|
||||
check_form!("a=b&a=", &[("a", "b"), ("a", "")]);
|
||||
check_form!("user=&password", &[("user", ""), ("password", "")]);
|
||||
check_form!("a=b&a", &[("a", "b"), ("a", "")]);
|
||||
|
||||
check_form!("user=x&&", &[("user", "x")]);
|
||||
check_form!("user=x&&&&pass=word", &[("user", "x"), ("pass", "word")]);
|
||||
check_form!("user=x&&&&pass=word&&&x=z&d&&&e",
|
||||
&[("user", "x"), ("pass", "word"), ("x", "z"), ("d", ""), ("e", "")]);
|
||||
|
||||
check_form!("=&a=b&&=", &[("a", "b")]);
|
||||
check_form!("=b", &[("", "b")]);
|
||||
check_form!("=b&=c", &[("", "b"), ("", "c")]);
|
||||
|
||||
check_form!("=", &[]);
|
||||
check_form!("&=&", &[]);
|
||||
check_form!("&", &[]);
|
||||
check_form!("=&=", &[]);
|
||||
|
||||
check_form!(@bad "=b&==");
|
||||
check_form!(@bad "==");
|
||||
check_form!(@bad "=k=");
|
||||
check_form!(@bad "=abc=");
|
||||
check_form!(@bad "=abc=cd");
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use super::FormItems;
|
||||
|
||||
// impl<'f> From<&'f [(&'f str, &'f str, &'f str)]> for FormItems<'f> {
|
||||
// #[inline(always)]
|
||||
// fn from(triples: &'f [(&'f str, &'f str, &'f str)]) -> FormItems<'f> {
|
||||
// // Safe because RawStr(str) is repr(transparent).
|
||||
// let triples = unsafe { ::std::mem::transmute(triples) };
|
||||
// FormItems::Cooked { triples, next_index: 0 }
|
||||
// }
|
||||
// }
|
||||
|
||||
// macro_rules! check_form {
|
||||
// (@bad $string:expr) => (check_form($string, None));
|
||||
// ($string:expr, $expected:expr) => (check_form(&$string[..], Some($expected)));
|
||||
// }
|
||||
|
||||
// fn check_form<'a, T>(items: T, expected: Option<&[(&str, &str, &str)]>)
|
||||
// where T: Into<FormItems<'a>> + ::std::fmt::Debug
|
||||
// {
|
||||
// let string = format!("{:?}", items);
|
||||
// let mut items = items.into();
|
||||
// let results: Vec<_> = items.by_ref().map(|item| item.explode()).collect();
|
||||
// if let Some(expected) = expected {
|
||||
// assert_eq!(expected.len(), results.len(),
|
||||
// "expected {:?}, got {:?} for {:?}", expected, results, string);
|
||||
|
||||
// for i in 0..results.len() {
|
||||
// let (expected_raw, expected_key, expected_val) = expected[i];
|
||||
// let (actual_raw, actual_key, actual_val) = results[i];
|
||||
|
||||
// assert!(actual_raw == expected_raw,
|
||||
// "raw [{}] mismatch for {}: expected {}, got {}",
|
||||
// i, string, expected_raw, actual_raw);
|
||||
|
||||
// assert!(actual_key == expected_key,
|
||||
// "key [{}] mismatch for {}: expected {}, got {}",
|
||||
// i, string, expected_key, actual_key);
|
||||
|
||||
// assert!(actual_val == expected_val,
|
||||
// "val [{}] mismatch for {}: expected {}, got {}",
|
||||
// i, string, expected_val, actual_val);
|
||||
// }
|
||||
// } else {
|
||||
// assert!(!items.exhaust(), "{} unexpectedly parsed successfully", string);
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_cooked_items() {
|
||||
// check_form!(
|
||||
// &[("username=user", "username", "user"), ("password=pass", "password", "pass")],
|
||||
// &[("username=user", "username", "user"), ("password=pass", "password", "pass")]
|
||||
// );
|
||||
|
||||
// let empty: &[(&str, &str, &str)] = &[];
|
||||
// check_form!(empty, &[]);
|
||||
|
||||
// check_form!(&[("a=b", "a", "b")], &[("a=b", "a", "b")]);
|
||||
|
||||
// check_form!(
|
||||
// &[("user=x", "user", "x"), ("pass=word", "pass", "word"),
|
||||
// ("x=z", "x", "z"), ("d=", "d", ""), ("e=", "e", "")],
|
||||
|
||||
// &[("user=x", "user", "x"), ("pass=word", "pass", "word"),
|
||||
// ("x=z", "x", "z"), ("d=", "d", ""), ("e=", "e", "")]
|
||||
// );
|
||||
// }
|
||||
|
||||
// // #[test]
|
||||
// // fn test_form_string() {
|
||||
// // check_form!("username=user&password=pass",
|
||||
// // &[("username", "user"), ("password", "pass")]);
|
||||
|
||||
// // check_form!("user=user&user=pass", &[("user", "user"), ("user", "pass")]);
|
||||
// // check_form!("user=&password=pass", &[("user", ""), ("password", "pass")]);
|
||||
// // check_form!("user&password=pass", &[("user", ""), ("password", "pass")]);
|
||||
// // check_form!("foo&bar", &[("foo", ""), ("bar", "")]);
|
||||
|
||||
// // check_form!("a=b", &[("a", "b")]);
|
||||
// // check_form!("value=Hello+World", &[("value", "Hello+World")]);
|
||||
|
||||
// // check_form!("user=", &[("user", "")]);
|
||||
// // check_form!("user=&", &[("user", "")]);
|
||||
// // check_form!("a=b&a=", &[("a", "b"), ("a", "")]);
|
||||
// // check_form!("user=&password", &[("user", ""), ("password", "")]);
|
||||
// // check_form!("a=b&a", &[("a", "b"), ("a", "")]);
|
||||
|
||||
// // check_form!("user=x&&", &[("user", "x")]);
|
||||
// // check_form!("user=x&&&&pass=word", &[("user", "x"), ("pass", "word")]);
|
||||
// // check_form!("user=x&&&&pass=word&&&x=z&d&&&e",
|
||||
// // &[("user", "x"), ("pass", "word"), ("x", "z"), ("d", ""), ("e", "")]);
|
||||
|
||||
// // check_form!("=&a=b&&=", &[("a", "b")]);
|
||||
// // check_form!("=b", &[("", "b")]);
|
||||
// // check_form!("=b&=c", &[("", "b"), ("", "c")]);
|
||||
|
||||
// // check_form!("=", &[]);
|
||||
// // check_form!("&=&", &[]);
|
||||
// // check_form!("&", &[]);
|
||||
// // check_form!("=&=", &[]);
|
||||
|
||||
// // check_form!(@bad "=b&==");
|
||||
// // check_form!(@bad "==");
|
||||
// // check_form!(@bad "=k=");
|
||||
// // check_form!(@bad "=abc=");
|
||||
// // check_form!(@bad "=abc=cd");
|
||||
// // }
|
||||
// }
|
||||
|
|
|
@ -14,8 +14,7 @@ use request::FormItems;
|
|||
/// validation.
|
||||
///
|
||||
/// ```rust
|
||||
/// #![feature(plugin, decl_macro)]
|
||||
/// #![plugin(rocket_codegen)]
|
||||
/// #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #![allow(deprecated, dead_code, unused_attributes)]
|
||||
///
|
||||
/// #[macro_use] extern crate rocket;
|
||||
|
@ -34,9 +33,8 @@ use request::FormItems;
|
|||
/// data via the `data` parameter and `Form` type.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #![allow(deprecated, dead_code, unused_attributes)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # use rocket::request::Form;
|
||||
/// # #[derive(FromForm)]
|
||||
|
@ -81,10 +79,10 @@ use request::FormItems;
|
|||
/// fn from_form(items: &mut FormItems<'f>, strict: bool) -> Result<Item, ()> {
|
||||
/// let mut field = None;
|
||||
///
|
||||
/// for (key, value) in items {
|
||||
/// match key.as_str() {
|
||||
/// for item in items {
|
||||
/// match item.key.as_str() {
|
||||
/// "balloon" | "space" if field.is_none() => {
|
||||
/// let decoded = value.url_decode().map_err(|_| ())?;
|
||||
/// let decoded = item.value.url_decode().map_err(|_| ())?;
|
||||
/// field = Some(decoded);
|
||||
/// }
|
||||
/// _ if strict => return Err(()),
|
||||
|
@ -115,16 +113,6 @@ pub trait FromForm<'f>: Sized {
|
|||
fn from_form(it: &mut FormItems<'f>, strict: bool) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
/// This implementation should only be used during debugging!
|
||||
impl<'f> FromForm<'f> for &'f str {
|
||||
type Error = !;
|
||||
|
||||
fn from_form(items: &mut FormItems<'f>, _: bool) -> Result<Self, !> {
|
||||
items.mark_complete();
|
||||
Ok(items.inner_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f, T: FromForm<'f>> FromForm<'f> for Option<T> {
|
||||
type Error = !;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::ops::Deref;
|
|||
|
||||
use request::{Request, form::{Form, FormDataError, FromForm}};
|
||||
use data::{Data, Transform, Transformed, FromData, Outcome};
|
||||
use http::uri::FromUriParam;
|
||||
|
||||
/// A data gaurd for parsing [`FromForm`] types leniently.
|
||||
///
|
||||
|
@ -30,9 +31,8 @@ use data::{Data, Transform, Transformed, FromData, Outcome};
|
|||
/// handler:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #![allow(deprecated, unused_attributes)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::request::LenientForm;
|
||||
///
|
||||
|
@ -60,7 +60,7 @@ use data::{Data, Transform, Transformed, FromData, Outcome};
|
|||
/// forms = 524288
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct LenientForm<T>(T);
|
||||
pub struct LenientForm<T>(crate T);
|
||||
|
||||
impl<T> LenientForm<T> {
|
||||
/// Consumes `self` and returns the parsed value.
|
||||
|
@ -68,8 +68,7 @@ impl<T> LenientForm<T> {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::request::LenientForm;
|
||||
///
|
||||
|
@ -110,3 +109,12 @@ impl<'f, T: FromForm<'f>> FromData<'f> for LenientForm<T> {
|
|||
<Form<T>>::from_data(o.borrowed()?, false).map(LenientForm)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f, A, T: FromUriParam<A> + FromForm<'f>> FromUriParam<A> for LenientForm<T> {
|
||||
type Target = T::Target;
|
||||
|
||||
#[inline(always)]
|
||||
fn from_uri_param(param: A) -> Self::Target {
|
||||
T::from_uri_param(param)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ mod lenient;
|
|||
mod error;
|
||||
mod form;
|
||||
|
||||
pub use self::form_items::FormItems;
|
||||
pub use self::form_items::{FormItems, FormItem};
|
||||
pub use self::from_form::FromForm;
|
||||
pub use self::from_form_value::FromFormValue;
|
||||
pub use self::form::Form;
|
||||
|
|
|
@ -166,9 +166,8 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
|
|||
/// `sensitive` handler.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// use rocket::Outcome;
|
||||
/// use rocket::http::Status;
|
||||
|
@ -222,9 +221,8 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
|
|||
/// routes (`admin_dashboard` and `user_dashboard`):
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, never_type)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// # use rocket::outcome::{IntoOutcome, Outcome};
|
||||
/// # use rocket::request::{self, FromRequest, Request};
|
||||
|
@ -285,9 +283,9 @@ impl<S, E> IntoOutcome<S, (Status, E), ()> for Result<S, E> {
|
|||
/// used, as illustrated below:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, never_type)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #![feature(never_type)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// # use rocket::outcome::{IntoOutcome, Outcome};
|
||||
/// # use rocket::request::{self, FromRequest, Request};
|
||||
|
|
|
@ -5,6 +5,7 @@ mod param;
|
|||
mod form;
|
||||
mod from_request;
|
||||
mod state;
|
||||
mod query;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
@ -12,9 +13,10 @@ mod tests;
|
|||
pub use self::request::Request;
|
||||
pub use self::from_request::{FromRequest, Outcome};
|
||||
pub use self::param::{FromParam, FromSegments};
|
||||
pub use self::form::{Form, LenientForm, FormItems};
|
||||
pub use self::form::{Form, LenientForm, FormItems, FormItem};
|
||||
pub use self::form::{FromForm, FormError, FromFormValue, FormParseError, FormDataError};
|
||||
pub use self::state::State;
|
||||
pub use self::query::{Query, FromQuery};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use response::flash::FlashMessage;
|
||||
|
|
|
@ -19,9 +19,8 @@ use http::{RawStr, uri::{Segments, SegmentError}};
|
|||
/// handler for the dynamic `"/<id>"` path:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #[get("/<id>")]
|
||||
/// fn hello(id: usize) -> String {
|
||||
/// # let _id = id;
|
||||
|
@ -55,9 +54,8 @@ use http::{RawStr, uri::{Segments, SegmentError}};
|
|||
/// parameter as follows:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # use rocket::http::RawStr;
|
||||
/// #[get("/<id>")]
|
||||
/// fn hello(id: Result<usize, &RawStr>) -> String {
|
||||
|
@ -168,9 +166,8 @@ use http::{RawStr, uri::{Segments, SegmentError}};
|
|||
/// dynamic path segment:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// # use rocket::request::FromParam;
|
||||
/// # use rocket::http::RawStr;
|
||||
/// # #[allow(dead_code)]
|
||||
|
@ -284,12 +281,14 @@ impl<'a, T: FromParam<'a>> FromParam<'a> for Option<T> {
|
|||
///
|
||||
/// # Provided Implementations
|
||||
///
|
||||
/// Rocket implements `FromParam` for `PathBuf`. The `PathBuf` implementation
|
||||
/// constructs a path from the segments iterator. Each segment is
|
||||
/// percent-decoded. If a segment equals ".." before or after decoding, the
|
||||
/// previous segment (if any) is omitted. For security purposes, any other
|
||||
/// segments that begin with "*" or "." are ignored. If a percent-decoded
|
||||
/// segment results in invalid UTF8, an `Err` is returned with the `Utf8Error`.
|
||||
/// **`PathBuf`**
|
||||
///
|
||||
/// The `PathBuf` implementation constructs a path from the segments iterator.
|
||||
/// Each segment is percent-decoded. If a segment equals ".." before or after
|
||||
/// decoding, the previous segment (if any) is omitted. For security purposes,
|
||||
/// any other segments that begin with "*" or "." are ignored. If a
|
||||
/// percent-decoded segment results in invalid UTF8, an `Err` is returned with
|
||||
/// the `Utf8Error`.
|
||||
pub trait FromSegments<'a>: Sized {
|
||||
/// The associated error to be returned when parsing fails.
|
||||
type Error: Debug;
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
use std::{slice::Iter, iter::Cloned};
|
||||
|
||||
use request::{FormItems, FormItem, Form, LenientForm, FromForm};
|
||||
|
||||
pub struct Query<'q>(pub &'q [FormItem<'q>]);
|
||||
|
||||
impl<'q> IntoIterator for Query<'q> {
|
||||
type Item = FormItem<'q>;
|
||||
type IntoIter = Cloned<Iter<'q, FormItem<'q>>>;
|
||||
|
||||
#[inline(always)]
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.iter().cloned()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FromQuery<'q>: Sized {
|
||||
type Error;
|
||||
|
||||
fn from_query(q: Query<'q>) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
impl<'q, T: FromForm<'q>> FromQuery<'q> for Form<T> {
|
||||
type Error = T::Error;
|
||||
|
||||
#[inline]
|
||||
fn from_query(q: Query<'q>) -> Result<Self, Self::Error> {
|
||||
T::from_form(&mut FormItems::from(q.0), true).map(Form)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q, T: FromForm<'q>> FromQuery<'q> for LenientForm<T> {
|
||||
type Error = <T as FromForm<'q>>::Error;
|
||||
|
||||
#[inline]
|
||||
fn from_query(q: Query<'q>) -> Result<Self, Self::Error> {
|
||||
T::from_form(&mut FormItems::from(q.0), false).map(LenientForm)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q, T: FromQuery<'q>> FromQuery<'q> for Option<T> {
|
||||
type Error = !;
|
||||
|
||||
#[inline]
|
||||
fn from_query(q: Query<'q>) -> Result<Self, Self::Error> {
|
||||
Ok(T::from_query(q).ok())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q, T: FromQuery<'q>> FromQuery<'q> for Result<T, T::Error> {
|
||||
type Error = !;
|
||||
|
||||
#[inline]
|
||||
fn from_query(q: Query<'q>) -> Result<Self, Self::Error> {
|
||||
Ok(T::from_query(q))
|
||||
}
|
||||
}
|
|
@ -7,27 +7,18 @@ use std::str;
|
|||
use yansi::Paint;
|
||||
use state::{Container, Storage};
|
||||
|
||||
use super::{FromParam, FromSegments, FromRequest, Outcome};
|
||||
use request::{FromParam, FromSegments, FromRequest, Outcome};
|
||||
use request::{FromFormValue, FormItems, FormItem};
|
||||
|
||||
use rocket::Rocket;
|
||||
use router::Route;
|
||||
use config::{Config, Limits};
|
||||
use http::uri::{Origin, Segments};
|
||||
use http::{Method, Header, HeaderMap, Cookies, CookieJar};
|
||||
use http::{RawStr, ContentType, Accept, MediaType};
|
||||
use http::{RawStr, ContentType, Accept, MediaType, Indexed, SmallVec};
|
||||
use http::hyper;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RequestState<'r> {
|
||||
config: &'r Config,
|
||||
managed: &'r Container,
|
||||
params: RefCell<Vec<(usize, usize)>>,
|
||||
route: Cell<Option<&'r Route>>,
|
||||
cookies: RefCell<CookieJar>,
|
||||
accept: Storage<Option<Accept>>,
|
||||
content_type: Storage<Option<ContentType>>,
|
||||
cache: Rc<Container>,
|
||||
}
|
||||
type Indices = (usize, usize);
|
||||
|
||||
/// The type of an incoming web request.
|
||||
///
|
||||
|
@ -42,7 +33,27 @@ pub struct Request<'r> {
|
|||
uri: Origin<'r>,
|
||||
headers: HeaderMap<'r>,
|
||||
remote: Option<SocketAddr>,
|
||||
state: RequestState<'r>,
|
||||
crate state: RequestState<'r>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
crate struct RequestState<'r> {
|
||||
crate config: &'r Config,
|
||||
crate managed: &'r Container,
|
||||
crate path_segments: SmallVec<[Indices; 12]>,
|
||||
crate query_items: Option<SmallVec<[IndexedFormItem; 6]>>,
|
||||
crate route: Cell<Option<&'r Route>>,
|
||||
crate cookies: RefCell<CookieJar>,
|
||||
crate accept: Storage<Option<Accept>>,
|
||||
crate content_type: Storage<Option<ContentType>>,
|
||||
crate cache: Rc<Container>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
crate struct IndexedFormItem {
|
||||
raw: Indices,
|
||||
key: Indices,
|
||||
value: Indices
|
||||
}
|
||||
|
||||
impl<'r> Request<'r> {
|
||||
|
@ -53,31 +64,26 @@ impl<'r> Request<'r> {
|
|||
method: Method,
|
||||
uri: Origin<'s>
|
||||
) -> Request<'r> {
|
||||
Request {
|
||||
let mut request = Request {
|
||||
method: Cell::new(method),
|
||||
uri: uri,
|
||||
headers: HeaderMap::new(),
|
||||
remote: None,
|
||||
state: RequestState {
|
||||
path_segments: SmallVec::new(),
|
||||
query_items: None,
|
||||
config: &rocket.config,
|
||||
managed: &rocket.state,
|
||||
route: Cell::new(None),
|
||||
params: RefCell::new(Vec::new()),
|
||||
cookies: RefCell::new(CookieJar::new()),
|
||||
accept: Storage::new(),
|
||||
content_type: Storage::new(),
|
||||
cache: Rc::new(Container::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Only used by doc-tests!
|
||||
#[doc(hidden)]
|
||||
pub fn example<F: Fn(&mut Request)>(method: Method, uri: &str, f: F) {
|
||||
let rocket = Rocket::custom(Config::development().unwrap());
|
||||
let uri = Origin::parse(uri).expect("invalid URI in example");
|
||||
let mut request = Request::new(&rocket, method, uri);
|
||||
f(&mut request);
|
||||
request.update_cached_uri_info();
|
||||
request
|
||||
}
|
||||
|
||||
/// Retrieve the method from `self`.
|
||||
|
@ -145,16 +151,14 @@ impl<'r> Request<'r> {
|
|||
/// # use rocket::http::Method;
|
||||
/// # Request::example(Method::Get, "/uri", |mut request| {
|
||||
/// let uri = Origin::parse("/hello/Sergio?type=greeting").unwrap();
|
||||
///
|
||||
/// request.set_uri(uri);
|
||||
/// assert_eq!(request.uri().path(), "/hello/Sergio");
|
||||
/// assert_eq!(request.uri().query(), Some("type=greeting"));
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline(always)]
|
||||
pub fn set_uri<'u: 'r>(&mut self, uri: Origin<'u>) {
|
||||
self.uri = uri;
|
||||
*self.state.params.borrow_mut() = Vec::new();
|
||||
self.update_cached_uri_info();
|
||||
}
|
||||
|
||||
/// Returns the address of the remote connection that initiated this
|
||||
|
@ -265,6 +269,40 @@ impl<'r> Request<'r> {
|
|||
self.real_ip().or_else(|| self.remote().map(|r| r.ip()))
|
||||
}
|
||||
|
||||
/// Returns a wrapped borrow to the cookies in `self`.
|
||||
///
|
||||
/// [`Cookies`](/rocket/http/enum.Cookies.html) implements internal
|
||||
/// mutability, so this method allows you to get _and_ add/remove cookies in
|
||||
/// `self`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Add a new cookie to a request's cookies:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket::Request;
|
||||
/// # use rocket::http::Method;
|
||||
/// use rocket::http::Cookie;
|
||||
///
|
||||
/// # Request::example(Method::Get, "/uri", |mut request| {
|
||||
/// request.cookies().add(Cookie::new("key", "val"));
|
||||
/// request.cookies().add(Cookie::new("ans", format!("life: {}", 38 + 4)));
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn cookies(&self) -> Cookies {
|
||||
// FIXME: Can we do better? This is disappointing.
|
||||
match self.state.cookies.try_borrow_mut() {
|
||||
Ok(jar) => Cookies::new(jar, self.state.config.secret_key()),
|
||||
Err(_) => {
|
||||
error_!("Multiple `Cookies` instances are active at once.");
|
||||
info_!("An instance of `Cookies` must be dropped before another \
|
||||
can be retrieved.");
|
||||
warn_!("The retrieved `Cookies` instance will be empty.");
|
||||
Cookies::empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [`HeaderMap`](/rocket/http/struct.HeaderMap.html) of all of
|
||||
/// the headers in `self`.
|
||||
///
|
||||
|
@ -334,40 +372,6 @@ impl<'r> Request<'r> {
|
|||
self.headers.replace(header.into());
|
||||
}
|
||||
|
||||
/// Returns a wrapped borrow to the cookies in `self`.
|
||||
///
|
||||
/// [`Cookies`](/rocket/http/enum.Cookies.html) implements internal
|
||||
/// mutability, so this method allows you to get _and_ add/remove cookies in
|
||||
/// `self`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Add a new cookie to a request's cookies:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket::Request;
|
||||
/// # use rocket::http::Method;
|
||||
/// use rocket::http::Cookie;
|
||||
///
|
||||
/// # Request::example(Method::Get, "/uri", |mut request| {
|
||||
/// request.cookies().add(Cookie::new("key", "val"));
|
||||
/// request.cookies().add(Cookie::new("ans", format!("life: {}", 38 + 4)));
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn cookies(&self) -> Cookies {
|
||||
// FIXME: Can we do better? This is disappointing.
|
||||
match self.state.cookies.try_borrow_mut() {
|
||||
Ok(jar) => Cookies::new(jar, self.state.config.secret_key()),
|
||||
Err(_) => {
|
||||
error_!("Multiple `Cookies` instances are active at once.");
|
||||
info_!("An instance of `Cookies` must be dropped before another \
|
||||
can be retrieved.");
|
||||
warn_!("The retrieved `Cookies` instance will be empty.");
|
||||
Cookies::empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Content-Type header of `self`. If the header is not present,
|
||||
/// returns `None`. The Content-Type header is cached after the first call
|
||||
/// to this function. As a result, subsequent calls will always return the
|
||||
|
@ -424,12 +428,11 @@ impl<'r> Request<'r> {
|
|||
}).as_ref()
|
||||
}
|
||||
|
||||
/// Returns the media type "format" of the request.
|
||||
/// Returns the media type "format" of the request if it is present.
|
||||
///
|
||||
/// The "format" of a request is either the Content-Type, if the request
|
||||
/// methods indicates support for a payload, or the preferred media type in
|
||||
/// the Accept header otherwise. If the method indicates no payload and no
|
||||
/// Accept header is specified, a media type of `Any` is returned.
|
||||
/// the Accept header otherwise.
|
||||
///
|
||||
/// The media type returned from this method is used to match against the
|
||||
/// `format` route attribute.
|
||||
|
@ -452,16 +455,13 @@ impl<'r> Request<'r> {
|
|||
/// # });
|
||||
/// ```
|
||||
pub fn format(&self) -> Option<&MediaType> {
|
||||
static ANY: MediaType = MediaType::Any;
|
||||
if self.method().supports_payload() {
|
||||
self.content_type().map(|ct| ct.media_type())
|
||||
} else {
|
||||
// FIXME: Should we be using `accept_first` or `preferred`? Or
|
||||
// should we be checking neither and instead pass things through
|
||||
// where the client accepts the thing at all?
|
||||
self.accept()
|
||||
.map(|accept| accept.preferred().media_type())
|
||||
.or(Some(&ANY))
|
||||
self.accept().map(|accept| accept.preferred().media_type())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -552,8 +552,8 @@ impl<'r> Request<'r> {
|
|||
})
|
||||
}
|
||||
|
||||
/// Retrieves and parses into `T` the 0-indexed `n`th dynamic parameter from
|
||||
/// the request. Returns `None` if `n` is greater than the number of params.
|
||||
/// Retrieves and parses into `T` the 0-indexed `n`th segment from the
|
||||
/// request. Returns `None` if `n` is greater than the number of segments.
|
||||
/// Returns `Some(Err(T::Error))` if the parameter type `T` failed to be
|
||||
/// parsed from the `n`th dynamic parameter.
|
||||
///
|
||||
|
@ -562,121 +562,221 @@ impl<'r> Request<'r> {
|
|||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Retrieve parameter `0`, which is expected to be a `String`, in a manual
|
||||
/// route:
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::{Request, Data};
|
||||
/// use rocket::handler::Outcome;
|
||||
/// # use rocket::{Request, http::Method};
|
||||
/// use rocket::http::{RawStr, uri::Origin};
|
||||
///
|
||||
/// # #[allow(dead_code)]
|
||||
/// fn name<'a>(req: &'a Request, _: Data) -> Outcome<'a> {
|
||||
/// let string = req.get_param::<String>(0)
|
||||
/// .and_then(|res| res.ok())
|
||||
/// .unwrap_or_else(|| "unnamed".into());
|
||||
/// # Request::example(Method::Get, "/", |req| {
|
||||
/// fn string<'s>(req: &'s mut Request, uri: &'static str, n: usize) -> &'s RawStr {
|
||||
/// req.set_uri(Origin::parse(uri).unwrap());
|
||||
///
|
||||
/// Outcome::from(req, string)
|
||||
/// req.get_param(n)
|
||||
/// .and_then(|r| r.ok())
|
||||
/// .unwrap_or("unnamed".into())
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(string(req, "/", 0).as_str(), "unnamed");
|
||||
/// assert_eq!(string(req, "/a/b/this_one", 0).as_str(), "a");
|
||||
/// assert_eq!(string(req, "/a/b/this_one", 1).as_str(), "b");
|
||||
/// assert_eq!(string(req, "/a/b/this_one", 2).as_str(), "this_one");
|
||||
/// assert_eq!(string(req, "/a/b/this_one", 3).as_str(), "unnamed");
|
||||
/// assert_eq!(string(req, "/a/b/c/d/e/f/g/h", 7).as_str(), "h");
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn get_param<'a, T>(&'a self, n: usize) -> Option<Result<T, T::Error>>
|
||||
where T: FromParam<'a>
|
||||
{
|
||||
Some(T::from_param(self.get_param_str(n)?))
|
||||
}
|
||||
|
||||
/// Get the `n`th path parameter as a string, if it exists. This is used by
|
||||
/// codegen.
|
||||
#[doc(hidden)]
|
||||
pub fn get_param_str(&self, n: usize) -> Option<&RawStr> {
|
||||
let params = self.state.params.borrow();
|
||||
if n >= params.len() {
|
||||
debug!("{} is >= param count {}", n, params.len());
|
||||
return None;
|
||||
}
|
||||
|
||||
let (i, j) = params[n];
|
||||
let path = self.uri.path();
|
||||
if j > path.len() {
|
||||
error!("Couldn't retrieve parameter: internal count incorrect.");
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(path[i..j].into())
|
||||
Some(T::from_param(self.raw_segment_str(n)?))
|
||||
}
|
||||
|
||||
/// Retrieves and parses into `T` all of the path segments in the request
|
||||
/// URI beginning at the 0-indexed `n`th dynamic parameter. `T` must
|
||||
/// implement [FromSegments](/rocket/request/trait.FromSegments.html), which
|
||||
/// is used to parse the segments.
|
||||
/// URI beginning and including the 0-indexed `n`th non-empty segment. `T`
|
||||
/// must implement [FromSegments](/rocket/request/trait.FromSegments.html),
|
||||
/// which is used to parse the segments.
|
||||
///
|
||||
/// This method exists only to be used by manual routing. To retrieve
|
||||
/// segments from a request, use Rocket's code generation facilities.
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
/// If there are less than `n` segments, returns `None`. If parsing the
|
||||
/// segments failed, returns `Some(Err(T:Error))`.
|
||||
/// If there are fewer than `n` non-empty segments, returns `None`. If
|
||||
/// parsing the segments failed, returns `Some(Err(T:Error))`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// If the request URI is `"/hello/there/i/am/here"`, and the matched route
|
||||
/// path for this request is `"/hello/<name>/i/<segs..>"`, then
|
||||
/// `request.get_segments::<T>(1)` will attempt to parse the segments
|
||||
/// `"am/here"` as type `T`.
|
||||
/// ```rust
|
||||
/// # use rocket::{Request, http::Method};
|
||||
/// use std::path::PathBuf;
|
||||
///
|
||||
/// use rocket::http::uri::Origin;
|
||||
///
|
||||
/// # Request::example(Method::Get, "/", |req| {
|
||||
/// fn path<'s>(req: &'s mut Request, uri: &'static str, n: usize) -> PathBuf {
|
||||
/// req.set_uri(Origin::parse(uri).unwrap());
|
||||
///
|
||||
/// req.get_segments(n)
|
||||
/// .and_then(|r| r.ok())
|
||||
/// .unwrap_or_else(|| "whoops".into())
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(path(req, "/", 0), PathBuf::from("whoops"));
|
||||
/// assert_eq!(path(req, "/a/", 0), PathBuf::from("a"));
|
||||
/// assert_eq!(path(req, "/a/b/c", 0), PathBuf::from("a/b/c"));
|
||||
/// assert_eq!(path(req, "/a/b/c", 1), PathBuf::from("b/c"));
|
||||
/// assert_eq!(path(req, "/a/b/c", 2), PathBuf::from("c"));
|
||||
/// assert_eq!(path(req, "/a/b/c", 6), PathBuf::from("whoops"));
|
||||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn get_segments<'a, T>(&'a self, n: usize) -> Option<Result<T, T::Error>>
|
||||
where T: FromSegments<'a>
|
||||
{
|
||||
Some(T::from_segments(self.get_raw_segments(n)?))
|
||||
Some(T::from_segments(self.raw_segments(n)?))
|
||||
}
|
||||
|
||||
/// Get the segments beginning at the `n`th dynamic parameter, if they
|
||||
/// exist. Used by codegen.
|
||||
#[doc(hidden)]
|
||||
pub fn get_raw_segments(&self, n: usize) -> Option<Segments> {
|
||||
let params = self.state.params.borrow();
|
||||
if n >= params.len() {
|
||||
debug!("{} is >= param (segments) count {}", n, params.len());
|
||||
return None;
|
||||
/// Retrieves and parses into `T` the query value with key `key`. `T` must
|
||||
/// implement [`FromFormValue`], which is used to parse the query's value.
|
||||
/// Key matching is performed case-sensitively. If there are multiple pairs
|
||||
/// with key `key`, the _last_ one is returned.
|
||||
///
|
||||
/// This method exists only to be used by manual routing. To retrieve
|
||||
/// query values from a request, use Rocket's code generation facilities.
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
/// If a query segment with key `key` isn't present, returns `None`. If
|
||||
/// parsing the value fails, returns `Some(Err(T:Error))`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use rocket::{Request, http::Method};
|
||||
/// use std::path::PathBuf;
|
||||
/// use rocket::http::{RawStr, uri::Origin};
|
||||
///
|
||||
/// # Request::example(Method::Get, "/", |req| {
|
||||
/// fn value<'s>(req: &'s mut Request, uri: &'static str, key: &str) -> &'s RawStr {
|
||||
/// req.set_uri(Origin::parse(uri).unwrap());
|
||||
///
|
||||
/// req.get_query_value(key)
|
||||
/// .and_then(|r| r.ok())
|
||||
/// .unwrap_or("n/a".into())
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(value(req, "/?a=apple&z=zebra", "a").as_str(), "apple");
|
||||
/// assert_eq!(value(req, "/?a=apple&z=zebra", "z").as_str(), "zebra");
|
||||
/// assert_eq!(value(req, "/?a=apple&z=zebra", "A").as_str(), "n/a");
|
||||
/// assert_eq!(value(req, "/?a=apple&z=zebra&a=argon", "a").as_str(), "argon");
|
||||
/// assert_eq!(value(req, "/?a=1&a=2&a=3&b=4", "a").as_str(), "3");
|
||||
/// assert_eq!(value(req, "/?a=apple&z=zebra", "apple").as_str(), "n/a");
|
||||
/// # });
|
||||
/// ```
|
||||
/// # Example
|
||||
///
|
||||
/// If the request query is `"/?apple=value_for_a&z=zebra"`, then
|
||||
/// `request.get_query_value::<T>("z")` will attempt to parse `"zebra"` as
|
||||
/// type `T`.
|
||||
#[inline]
|
||||
pub fn get_query_value<'a, T>(&'a self, key: &str) -> Option<Result<T, T::Error>>
|
||||
where T: FromFormValue<'a>
|
||||
{
|
||||
self.raw_query_items()?
|
||||
.rev()
|
||||
.find(|item| item.key.as_str() == key)
|
||||
.map(|item| T::from_form_value(item.value))
|
||||
}
|
||||
}
|
||||
|
||||
// All of these methods only exist for internal, including codegen, purposes.
|
||||
// They _are not_ part of the stable API.
|
||||
#[doc(hidden)]
|
||||
impl<'r> Request<'r> {
|
||||
// Only used by doc-tests! Needs to be `pub` because doc-test are external.
|
||||
pub fn example<F: Fn(&mut Request)>(method: Method, uri: &str, f: F) {
|
||||
let rocket = Rocket::custom(Config::development().unwrap());
|
||||
let uri = Origin::parse(uri).expect("invalid URI in example");
|
||||
let mut request = Request::new(&rocket, method, uri);
|
||||
f(&mut request);
|
||||
}
|
||||
|
||||
let (i, j) = params[n];
|
||||
// Updates the cached `path_segments` and `query_items` in `self.state`.
|
||||
// MUST be called whenever a new URI is set or updated.
|
||||
#[inline]
|
||||
fn update_cached_uri_info(&mut self) {
|
||||
let path_segments = Segments(self.uri.path())
|
||||
.map(|s| indices(s, self.uri.path()))
|
||||
.collect();
|
||||
|
||||
let query_items = self.uri.query()
|
||||
.map(|query_str| FormItems::from(query_str)
|
||||
.map(|item| IndexedFormItem::from(query_str, item))
|
||||
.collect()
|
||||
);
|
||||
|
||||
self.state.path_segments = path_segments;
|
||||
self.state.query_items = query_items;
|
||||
}
|
||||
|
||||
/// Get the `n`th path segment, 0-indexed, after the mount point for the
|
||||
/// currently matched route, as a string, if it exists. Used by codegen.
|
||||
#[inline]
|
||||
pub fn raw_segment_str(&self, n: usize) -> Option<&RawStr> {
|
||||
self.routed_path_segment(n)
|
||||
.map(|(i, j)| self.uri.path()[i..j].into())
|
||||
}
|
||||
|
||||
/// Get the segments beginning at the `n`th, 0-indexed, after the mount
|
||||
/// point for the currently matched route, if they exist. Used by codegen.
|
||||
#[inline]
|
||||
pub fn raw_segments(&self, n: usize) -> Option<Segments> {
|
||||
self.routed_path_segment(n)
|
||||
.map(|(i, _)| Segments(&self.uri.path()[i..]) )
|
||||
}
|
||||
|
||||
// Returns an iterator over the raw segments of the path URI. Does not take
|
||||
// into account the current route. This is used during routing.
|
||||
#[inline]
|
||||
crate fn raw_path_segments(&self) -> impl Iterator<Item = &RawStr> {
|
||||
let path = self.uri.path();
|
||||
if j > path.len() {
|
||||
error!("Couldn't retrieve segments: internal count incorrect.");
|
||||
return None;
|
||||
self.state.path_segments.iter().cloned()
|
||||
.map(move |(i, j)| path[i..j].into())
|
||||
}
|
||||
|
||||
Some(Segments(&path[i..j]))
|
||||
#[inline]
|
||||
fn routed_path_segment(&self, n: usize) -> Option<(usize, usize)> {
|
||||
let mount_segments = self.route()
|
||||
.map(|r| r.base.segment_count())
|
||||
.unwrap_or(0);
|
||||
|
||||
self.state.path_segments.get(mount_segments + n).map(|(i, j)| (*i, *j))
|
||||
}
|
||||
|
||||
// Retrieves the pre-parsed query items. Used by matching and codegen.
|
||||
#[inline]
|
||||
pub fn raw_query_items(
|
||||
&self
|
||||
) -> Option<impl Iterator<Item = FormItem> + DoubleEndedIterator + Clone> {
|
||||
let query = self.uri.query()?;
|
||||
self.state.query_items.as_ref().map(move |items| {
|
||||
items.iter().map(move |item| item.convert(query))
|
||||
})
|
||||
}
|
||||
|
||||
/// Set `self`'s parameters given that the route used to reach this request
|
||||
/// was `route`. This should only be used internally by `Rocket` as improper
|
||||
/// use may result in out of bounds indexing.
|
||||
/// TODO: Figure out the mount path from here.
|
||||
#[inline]
|
||||
/// was `route`. Use during routing when attempting a given route.
|
||||
#[inline(always)]
|
||||
crate fn set_route(&self, route: &'r Route) {
|
||||
self.state.route.set(Some(route));
|
||||
*self.state.params.borrow_mut() = route.get_param_indexes(self.uri());
|
||||
}
|
||||
|
||||
/// Set the method of `self`, even when `self` is a shared reference.
|
||||
/// Set the method of `self`, even when `self` is a shared reference. Used
|
||||
/// during routing to override methods for re-routing.
|
||||
#[inline(always)]
|
||||
crate fn _set_method(&self, method: Method) {
|
||||
self.method.set(method);
|
||||
}
|
||||
|
||||
/// Replace all of the cookies in `self` with those in `jar`.
|
||||
#[inline]
|
||||
crate fn set_cookies(&mut self, jar: CookieJar) {
|
||||
self.state.cookies = RefCell::new(jar);
|
||||
}
|
||||
|
||||
/// Get the managed state T, if it exists. For internal use only!
|
||||
#[inline(always)]
|
||||
crate fn get_state<T: Send + Sync + 'static>(&self) -> Option<&'r T> {
|
||||
self.state.managed.try_get()
|
||||
}
|
||||
|
||||
/// Convert from Hyper types into a Rocket Request.
|
||||
crate fn from_hyp(
|
||||
rocket: &'r Rocket,
|
||||
|
@ -720,7 +820,7 @@ impl<'r> Request<'r> {
|
|||
}
|
||||
}
|
||||
|
||||
request.set_cookies(cookie_jar);
|
||||
request.state.cookies = RefCell::new(cookie_jar);
|
||||
}
|
||||
|
||||
// Set the rest of the headers.
|
||||
|
@ -766,3 +866,26 @@ impl<'r> fmt::Display for Request<'r> {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexedFormItem {
|
||||
#[inline(always)]
|
||||
fn from(s: &str, i: FormItem) -> Self {
|
||||
let (r, k, v) = (indices(i.raw, s), indices(i.key, s), indices(i.value, s));
|
||||
IndexedFormItem { raw: r, key: k, value: v }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn convert<'s>(&self, source: &'s str) -> FormItem<'s> {
|
||||
FormItem {
|
||||
raw: source[self.raw.0..self.raw.1].into(),
|
||||
key: source[self.key.0..self.key.1].into(),
|
||||
value: source[self.value.0..self.value.1].into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn indices(needle: &str, haystack: &str) -> (usize, usize) {
|
||||
Indexed::checked_from(needle, haystack)
|
||||
.expect("segments inside of path/query")
|
||||
.indices()
|
||||
}
|
||||
|
|
|
@ -21,8 +21,7 @@ use http::Status;
|
|||
/// following example does just this:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::State;
|
||||
///
|
||||
|
@ -123,7 +122,7 @@ impl<'a, 'r, T: Send + Sync + 'static> FromRequest<'a, 'r> for State<'r, T> {
|
|||
|
||||
#[inline(always)]
|
||||
fn from_request(req: &'a Request<'r>) -> request::Outcome<State<'r, T>, ()> {
|
||||
match req.get_state::<T>() {
|
||||
match req.state.managed.try_get::<T>() {
|
||||
Some(state) => Outcome::Success(State(state)),
|
||||
None => {
|
||||
error_!("Attempted to retrieve unmanaged state!");
|
||||
|
|
|
@ -47,11 +47,8 @@ const FLASH_COOKIE_NAME: &str = "_flash";
|
|||
/// message on both the request and response sides.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// #
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// use rocket::response::{Flash, Redirect};
|
||||
/// use rocket::request::FlashMessage;
|
||||
/// use rocket::http::RawStr;
|
||||
|
|
|
@ -151,9 +151,8 @@ use request::Request;
|
|||
/// following `Responder` implementation accomplishes this:
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// # #[derive(Debug)]
|
||||
/// # struct Person { name: String, age: u16 }
|
||||
|
|
|
@ -185,8 +185,8 @@ impl Rocket {
|
|||
if is_form && req.method() == Method::Post && data_len >= min_len {
|
||||
if let Ok(form) = from_utf8(&data.peek()[..min(data_len, max_len)]) {
|
||||
let method: Option<Result<Method, _>> = FormItems::from(form)
|
||||
.filter(|&(key, _)| key.as_str() == "_method")
|
||||
.map(|(_, value)| value.parse())
|
||||
.filter(|item| item.key.as_str() == "_method")
|
||||
.map(|item| item.value.parse())
|
||||
.next();
|
||||
|
||||
if let Some(Ok(method)) = method {
|
||||
|
@ -455,8 +455,7 @@ impl Rocket {
|
|||
/// dispatched to the `hi` route.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// #
|
||||
/// #[get("/world")]
|
||||
|
@ -497,11 +496,6 @@ impl Rocket {
|
|||
Paint::purple("Mounting"),
|
||||
Paint::blue(base));
|
||||
|
||||
if base.contains('<') || base.contains('>') {
|
||||
error_!("Mount point '{}' contains dynamic paramters.", base);
|
||||
panic!("Invalid mount point.");
|
||||
}
|
||||
|
||||
let base_uri = Origin::parse(base)
|
||||
.unwrap_or_else(|e| {
|
||||
error_!("Invalid origin URI '{}' used as mount point.", base);
|
||||
|
@ -513,22 +507,12 @@ impl Rocket {
|
|||
panic!("Invalid mount point.");
|
||||
}
|
||||
|
||||
if !base_uri.is_normalized() {
|
||||
error_!("Mount point '{}' is not normalized.", base_uri);
|
||||
info_!("Expected: '{}'.", base_uri.to_normalized());
|
||||
panic!("Invalid mount point.");
|
||||
}
|
||||
|
||||
for mut route in routes.into() {
|
||||
let complete_uri = format!("{}/{}", base_uri, route.uri);
|
||||
let uri = Origin::parse_route(&complete_uri)
|
||||
.unwrap_or_else(|e| {
|
||||
error_!("Invalid route URI: {}", base);
|
||||
panic!("Error: {}", e)
|
||||
});
|
||||
|
||||
route.set_base(base_uri.clone());
|
||||
route.set_uri(uri.to_normalized());
|
||||
let path = route.uri.clone();
|
||||
if let Err(e) = route.set_uri(base_uri.clone(), path) {
|
||||
error_!("{}", e);
|
||||
panic!("Invalid route URI.");
|
||||
}
|
||||
|
||||
info_!("{}", route);
|
||||
self.router.add(route);
|
||||
|
@ -542,11 +526,8 @@ impl Rocket {
|
|||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
/// #![plugin(rocket_codegen)]
|
||||
///
|
||||
/// #[macro_use] extern crate rocket;
|
||||
///
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::Request;
|
||||
///
|
||||
/// #[catch(500)]
|
||||
|
@ -601,8 +582,7 @@ impl Rocket {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::State;
|
||||
///
|
||||
|
@ -639,9 +619,8 @@ impl Rocket {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::Rocket;
|
||||
/// use rocket::fairing::AdHoc;
|
||||
///
|
||||
|
@ -756,8 +735,7 @@ impl Rocket {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::Rocket;
|
||||
/// use rocket::fairing::AdHoc;
|
||||
|
@ -813,9 +791,8 @@ impl Rocket {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![feature(plugin, decl_macro)]
|
||||
/// # #![plugin(rocket_codegen)]
|
||||
/// # extern crate rocket;
|
||||
/// # #![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
/// # #[macro_use] extern crate rocket;
|
||||
/// use rocket::Rocket;
|
||||
/// use rocket::fairing::AdHoc;
|
||||
///
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use super::Route;
|
||||
|
||||
use http::uri::Origin;
|
||||
use http::MediaType;
|
||||
use http::route::Kind;
|
||||
use request::Request;
|
||||
|
||||
impl Route {
|
||||
|
@ -15,22 +15,15 @@ impl Route {
|
|||
/// * If route specifies a format, it only gets requests for that format.
|
||||
/// * If route doesn't specify a format, it gets requests for any format.
|
||||
///
|
||||
/// Query collisions work like this:
|
||||
///
|
||||
/// * If routes specify a query, they only gets request that have queries.
|
||||
/// * If routes don't specify a query, requests with queries also match.
|
||||
///
|
||||
/// As a result, as long as everything else collides, whether a route has a
|
||||
/// query or not is irrelevant: it will collide.
|
||||
/// Because query parsing is lenient, and dynamic query parameters can be
|
||||
/// missing, queries do not impact whether two routes collide.
|
||||
pub fn collides_with(&self, other: &Route) -> bool {
|
||||
self.method == other.method
|
||||
&& self.rank == other.rank
|
||||
&& paths_collide(&self.uri, &other.uri)
|
||||
&& paths_collide(self, other)
|
||||
&& match (self.format.as_ref(), other.format.as_ref()) {
|
||||
(Some(a), Some(b)) => media_types_collide(a, b),
|
||||
(Some(_), None) => true,
|
||||
(None, Some(_)) => true,
|
||||
(None, None) => true
|
||||
_ => true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,21 +36,13 @@ impl Route {
|
|||
/// - If route doesn't specify format, it gets requests for any format.
|
||||
/// * All static components in the route's path match the corresponding
|
||||
/// components in the same position in the incoming request.
|
||||
/// * If the route specifies a query, the request must have a query as
|
||||
/// well. If the route doesn't specify a query, requests with and
|
||||
/// without queries match.
|
||||
///
|
||||
/// In the future, query handling will work as follows:
|
||||
///
|
||||
/// * All static components in the route's query string are also in the
|
||||
/// request query string, though in any position, and there exists a
|
||||
/// query parameter named exactly like each non-multi dynamic component
|
||||
/// in the route's query that wasn't matched against a static component.
|
||||
/// request query string, though in any position.
|
||||
/// - If no query in route, requests with/without queries match.
|
||||
pub fn matches(&self, req: &Request) -> bool {
|
||||
self.method == req.method()
|
||||
&& paths_collide(&self.uri, req.uri())
|
||||
&& queries_collide(self, req)
|
||||
&& paths_match(self, req)
|
||||
&& queries_match(self, req)
|
||||
&& match self.format {
|
||||
Some(ref a) => match req.format() {
|
||||
Some(ref b) => media_types_collide(a, b),
|
||||
|
@ -68,51 +53,69 @@ impl Route {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn iters_match_until<A, B>(break_c: u8, mut a: A, mut b: B) -> bool
|
||||
where A: Iterator<Item = u8>, B: Iterator<Item = u8>
|
||||
{
|
||||
loop {
|
||||
match (a.next(), b.next()) {
|
||||
(None, Some(_)) => return false,
|
||||
(Some(_), None) => return false,
|
||||
(None, None) => return true,
|
||||
(Some(c1), Some(c2)) if c1 == break_c || c2 == break_c => return true,
|
||||
(Some(c1), Some(c2)) if c1 != c2 => return false,
|
||||
(Some(_), Some(_)) => continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn segments_collide(first: &str, other: &str) -> bool {
|
||||
let a_iter = first.as_bytes().iter().cloned();
|
||||
let b_iter = other.as_bytes().iter().cloned();
|
||||
iters_match_until(b'<', a_iter.clone(), b_iter.clone())
|
||||
&& iters_match_until(b'>', a_iter.rev(), b_iter.rev())
|
||||
}
|
||||
|
||||
fn paths_collide(first: &Origin, other: &Origin) -> bool {
|
||||
for (seg_a, seg_b) in first.segments().zip(other.segments()) {
|
||||
if seg_a.ends_with("..>") || seg_b.ends_with("..>") {
|
||||
fn paths_collide(route: &Route, other: &Route) -> bool {
|
||||
let a_segments = &route.metadata.path_segments;
|
||||
let b_segments = &other.metadata.path_segments;
|
||||
for (seg_a, seg_b) in a_segments.iter().zip(b_segments.iter()) {
|
||||
if seg_a.kind == Kind::Multi || seg_b.kind == Kind::Multi {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !segments_collide(seg_a, seg_b) {
|
||||
if seg_a.kind == Kind::Static && seg_b.kind == Kind::Static {
|
||||
if seg_a.string != seg_b.string {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if first.segment_count() != other.segment_count() {
|
||||
a_segments.len() == b_segments.len()
|
||||
}
|
||||
|
||||
fn paths_match(route: &Route, request: &Request) -> bool {
|
||||
let route_segments = &route.metadata.path_segments;
|
||||
if route_segments.len() > request.state.path_segments.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let request_segments = request.raw_path_segments();
|
||||
for (route_seg, req_seg) in route_segments.iter().zip(request_segments) {
|
||||
match route_seg.kind {
|
||||
Kind::Multi => return true,
|
||||
Kind::Static if &*route_seg.string != req_seg.as_str() => return false,
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
route_segments.len() == request.state.path_segments.len()
|
||||
}
|
||||
|
||||
fn queries_match(route: &Route, request: &Request) -> bool {
|
||||
if route.metadata.fully_dynamic_query {
|
||||
return true;
|
||||
}
|
||||
|
||||
let route_query_segments = match route.metadata.query_segments {
|
||||
Some(ref segments) => segments,
|
||||
None => return true
|
||||
};
|
||||
|
||||
let req_query_segments = match request.raw_query_items() {
|
||||
Some(iter) => iter.map(|item| item.raw.as_str()),
|
||||
None => return route.metadata.fully_dynamic_query
|
||||
};
|
||||
|
||||
for seg in route_query_segments.iter() {
|
||||
if seg.kind == Kind::Static {
|
||||
// it's okay; this clones the iterator
|
||||
if !req_query_segments.clone().any(|r| r == seg.string) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn queries_collide(route: &Route, req: &Request) -> bool {
|
||||
route.uri.query().map_or(true, |_| req.uri().query().is_some())
|
||||
}
|
||||
|
||||
fn media_types_collide(first: &MediaType, other: &MediaType) -> bool {
|
||||
let collide = |a, b| a == "*" || b == "*" || a == b;
|
||||
collide(first.top(), other.top()) && collide(first.sub(), other.sub())
|
||||
|
@ -134,20 +137,20 @@ mod tests {
|
|||
type SimpleRoute = (Method, &'static str);
|
||||
|
||||
fn m_collide(a: SimpleRoute, b: SimpleRoute) -> bool {
|
||||
let route_a = Route::new(a.0, a.1.to_string(), dummy_handler);
|
||||
route_a.collides_with(&Route::new(b.0, b.1.to_string(), dummy_handler))
|
||||
let route_a = Route::new(a.0, a.1, dummy_handler);
|
||||
route_a.collides_with(&Route::new(b.0, b.1, dummy_handler))
|
||||
}
|
||||
|
||||
fn unranked_collide(a: &'static str, b: &'static str) -> bool {
|
||||
let route_a = Route::ranked(0, Get, a.to_string(), dummy_handler);
|
||||
let route_b = Route::ranked(0, Get, b.to_string(), dummy_handler);
|
||||
let route_a = Route::ranked(0, Get, a, dummy_handler);
|
||||
let route_b = Route::ranked(0, Get, b, dummy_handler);
|
||||
eprintln!("Checking {} against {}.", route_a, route_b);
|
||||
route_a.collides_with(&route_b)
|
||||
}
|
||||
|
||||
fn s_s_collide(a: &'static str, b: &'static str) -> bool {
|
||||
let a = Origin::parse_route(a).unwrap();
|
||||
let b = Origin::parse_route(b).unwrap();
|
||||
let a = Route::new(Get, a, dummy_handler);
|
||||
let b = Route::new(Get, b, dummy_handler);
|
||||
paths_collide(&a, &b)
|
||||
}
|
||||
|
||||
|
@ -187,15 +190,10 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn hard_param_collisions() {
|
||||
assert!(unranked_collide("/<name>bob", "/<name>b"));
|
||||
assert!(unranked_collide("/a<b>c", "/abc"));
|
||||
assert!(unranked_collide("/a<b>c", "/azooc"));
|
||||
assert!(unranked_collide("/a<b>", "/ab"));
|
||||
assert!(unranked_collide("/<b>", "/a"));
|
||||
assert!(unranked_collide("/<a>/<b>", "/a/b<c>"));
|
||||
assert!(unranked_collide("/<a>/bc<b>", "/a/b<c>"));
|
||||
assert!(unranked_collide("/<a>/bc<b>d", "/a/b<c>"));
|
||||
assert!(unranked_collide("/<a..>", "///a///"));
|
||||
assert!(unranked_collide("/<a..>", "//a/bcjdklfj//<c>"));
|
||||
assert!(unranked_collide("/a/<a..>", "//a/bcjdklfj//<c>"));
|
||||
assert!(unranked_collide("/a/<b>/<c..>", "//a/bcjdklfj//<c>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -222,12 +220,7 @@ mod tests {
|
|||
assert!(!unranked_collide("/a/hello", "/a/c"));
|
||||
assert!(!unranked_collide("/hello", "/a/c"));
|
||||
assert!(!unranked_collide("/hello/there", "/hello/there/guy"));
|
||||
assert!(!unranked_collide("/b<a>/there", "/hi/there"));
|
||||
assert!(!unranked_collide("/<a>/<b>c", "/hi/person"));
|
||||
assert!(!unranked_collide("/<a>/<b>cd", "/hi/<a>e"));
|
||||
assert!(!unranked_collide("/a<a>/<b>", "/b<b>/<a>"));
|
||||
assert!(!unranked_collide("/a/<b>", "/b/<b>"));
|
||||
assert!(!unranked_collide("/a<a>/<b>", "/b/<b>"));
|
||||
assert!(!unranked_collide("/<a..>", "/"));
|
||||
assert!(!unranked_collide("/hi/<a..>", "/hi"));
|
||||
assert!(!unranked_collide("/hi/<a..>", "/hi/"));
|
||||
|
@ -272,36 +265,21 @@ mod tests {
|
|||
assert!(!s_s_collide("/a/hello", "/a/c"));
|
||||
assert!(!s_s_collide("/hello", "/a/c"));
|
||||
assert!(!s_s_collide("/hello/there", "/hello/there/guy"));
|
||||
assert!(!s_s_collide("/b<a>/there", "/hi/there"));
|
||||
assert!(!s_s_collide("/<a>/<b>c", "/hi/person"));
|
||||
assert!(!s_s_collide("/<a>/<b>cd", "/hi/<a>e"));
|
||||
assert!(!s_s_collide("/a<a>/<b>", "/b<b>/<a>"));
|
||||
assert!(!s_s_collide("/a/<b>", "/b/<b>"));
|
||||
assert!(!s_s_collide("/a<a>/<b>", "/b/<b>"));
|
||||
assert!(!s_s_collide("/a", "/b"));
|
||||
assert!(!s_s_collide("/a/b", "/a"));
|
||||
assert!(!s_s_collide("/a/b", "/a/c"));
|
||||
assert!(!s_s_collide("/a/hello", "/a/c"));
|
||||
assert!(!s_s_collide("/hello", "/a/c"));
|
||||
assert!(!s_s_collide("/hello/there", "/hello/there/guy"));
|
||||
assert!(!s_s_collide("/b<a>/there", "/hi/there"));
|
||||
assert!(!s_s_collide("/<a>/<b>c", "/hi/person"));
|
||||
assert!(!s_s_collide("/<a>/<b>cd", "/hi/<a>e"));
|
||||
assert!(!s_s_collide("/a<a>/<b>", "/b<b>/<a>"));
|
||||
assert!(!s_s_collide("/a/<b>", "/b/<b>"));
|
||||
assert!(!s_s_collide("/a<a>/<b>", "/b/<b>"));
|
||||
assert!(!s_s_collide("/a", "/b"));
|
||||
assert!(!s_s_collide("/a/b", "/a"));
|
||||
assert!(!s_s_collide("/a/b", "/a/c"));
|
||||
assert!(!s_s_collide("/a/hello", "/a/c"));
|
||||
assert!(!s_s_collide("/hello", "/a/c"));
|
||||
assert!(!s_s_collide("/hello/there", "/hello/there/guy"));
|
||||
assert!(!s_s_collide("/b<a>/there", "/hi/there"));
|
||||
assert!(!s_s_collide("/<a>/<b>c", "/hi/person"));
|
||||
assert!(!s_s_collide("/<a>/<b>cd", "/hi/<a>e"));
|
||||
assert!(!s_s_collide("/a<a>/<b>", "/b<b>/<a>"));
|
||||
assert!(!s_s_collide("/a/<b>", "/b/<b>"));
|
||||
assert!(!s_s_collide("/a<a>/<b>", "/b/<b>"));
|
||||
assert!(!s_s_collide("/<a..>", "/"));
|
||||
assert!(!s_s_collide("/hi/<a..>", "/hi/"));
|
||||
assert!(!s_s_collide("/a/hi/<a..>", "/a/hi/"));
|
||||
|
@ -432,7 +410,7 @@ mod tests {
|
|||
assert!(!req_route_mt_collide(Post, None, "application/json"));
|
||||
}
|
||||
|
||||
fn req_route_path_collide(a: &'static str, b: &'static str) -> bool {
|
||||
fn req_route_path_match(a: &'static str, b: &'static str) -> bool {
|
||||
let rocket = Rocket::custom(Config::development().unwrap());
|
||||
let req = Request::new(&rocket, Get, Origin::parse(a).expect("valid URI"));
|
||||
let route = Route::ranked(0, Get, b.to_string(), dummy_handler);
|
||||
|
@ -441,21 +419,37 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_req_route_query_collisions() {
|
||||
assert!(req_route_path_collide("/a/b?a=b", "/a/b?<c>"));
|
||||
assert!(req_route_path_collide("/a/b?a=b", "/<a>/b?<c>"));
|
||||
assert!(req_route_path_collide("/a/b?a=b", "/<a>/<b>?<c>"));
|
||||
assert!(req_route_path_collide("/a/b?a=b", "/a/<b>?<c>"));
|
||||
assert!(req_route_path_collide("/?b=c", "/?<b>"));
|
||||
assert!(req_route_path_match("/a/b?a=b", "/a/b?<c>"));
|
||||
assert!(req_route_path_match("/a/b?a=b", "/<a>/b?<c>"));
|
||||
assert!(req_route_path_match("/a/b?a=b", "/<a>/<b>?<c>"));
|
||||
assert!(req_route_path_match("/a/b?a=b", "/a/<b>?<c>"));
|
||||
assert!(req_route_path_match("/?b=c", "/?<b>"));
|
||||
|
||||
assert!(req_route_path_collide("/a/b?a=b", "/a/b"));
|
||||
assert!(req_route_path_collide("/a/b", "/a/b"));
|
||||
assert!(req_route_path_collide("/a/b/c/d?", "/a/b/c/d"));
|
||||
assert!(req_route_path_collide("/a/b/c/d?v=1&v=2", "/a/b/c/d"));
|
||||
assert!(req_route_path_match("/a/b?a=b", "/a/b"));
|
||||
assert!(req_route_path_match("/a/b", "/a/b"));
|
||||
assert!(req_route_path_match("/a/b/c/d?", "/a/b/c/d"));
|
||||
assert!(req_route_path_match("/a/b/c/d?v=1&v=2", "/a/b/c/d"));
|
||||
|
||||
assert!(!req_route_path_collide("/a/b", "/a/b?<c>"));
|
||||
assert!(!req_route_path_collide("/a/b/c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_collide("/a?b=c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_collide("/?b=c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_collide("/?b=c", "/a?<c>"));
|
||||
assert!(req_route_path_match("/a/b", "/a/b?<c>"));
|
||||
assert!(req_route_path_match("/a/b", "/a/b?<c..>"));
|
||||
assert!(req_route_path_match("/a/b?c", "/a/b?c"));
|
||||
assert!(req_route_path_match("/a/b?c", "/a/b?<c>"));
|
||||
assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?<c>"));
|
||||
assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?<c..>"));
|
||||
|
||||
assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?c=foo&<c..>"));
|
||||
assert!(req_route_path_match("/a/b?c=foo&d=z", "/a/b?d=z&<c..>"));
|
||||
|
||||
assert!(!req_route_path_match("/a/b/c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_match("/a?b=c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_match("/?b=c", "/a/b?<c>"));
|
||||
assert!(!req_route_path_match("/?b=c", "/a?<c>"));
|
||||
|
||||
assert!(!req_route_path_match("/a/b?c=foo&d=z", "/a/b?a=b&<c..>"));
|
||||
assert!(!req_route_path_match("/a/b?c=foo&d=z", "/a/b?d=b&<c..>"));
|
||||
assert!(!req_route_path_match("/a/b", "/a/b?c"));
|
||||
assert!(!req_route_path_match("/a/b", "/a/b?foo"));
|
||||
assert!(!req_route_path_match("/a/b", "/a/b?foo&<rest..>"));
|
||||
assert!(!req_route_path_match("/a/b", "/a/b?<a>&b&<rest..>"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ crate fn dummy_handler<'r>(r: &'r ::Request, _: ::Data) -> ::handler::Outcome<'r
|
|||
|
||||
#[derive(Default)]
|
||||
pub struct Router {
|
||||
routes: HashMap<Selector, Vec<Route>>, // using 'selector' for now
|
||||
routes: HashMap<Selector, Vec<Route>>,
|
||||
}
|
||||
|
||||
impl Router {
|
||||
|
@ -29,7 +29,9 @@ impl Router {
|
|||
pub fn add(&mut self, route: Route) {
|
||||
let selector = route.method;
|
||||
let entries = self.routes.entry(selector).or_insert_with(|| vec![]);
|
||||
let i = entries.binary_search_by_key(&route.rank, |r| r.rank).unwrap_or_else(|i| i);
|
||||
let i = entries.binary_search_by_key(&route.rank, |r| r.rank)
|
||||
.unwrap_or_else(|i| i);
|
||||
|
||||
entries.insert(i, route);
|
||||
}
|
||||
|
||||
|
@ -179,6 +181,19 @@ mod test {
|
|||
assert!(unranked_route_collisions(&["/a//<a..>//", "/a/b//c//d/e/"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collisions_query() {
|
||||
// Query shouldn't affect things when unranked.
|
||||
assert!(unranked_route_collisions(&["/hello?<foo>", "/hello"]));
|
||||
assert!(unranked_route_collisions(&["/<a>?foo=bar", "/hello?foo=bar&cat=fat"]));
|
||||
assert!(unranked_route_collisions(&["/<a>?foo=bar", "/hello?foo=bar&cat=fat"]));
|
||||
assert!(unranked_route_collisions(&["/<a>", "/<b>?<foo>"]));
|
||||
assert!(unranked_route_collisions(&["/hello/bob?a=b", "/hello/<b>?d=e"]));
|
||||
assert!(unranked_route_collisions(&["/<foo>?a=b", "/foo?d=e"]));
|
||||
assert!(unranked_route_collisions(&["/<foo>?a=b&<c>", "/<foo>?d=e&<c>"]));
|
||||
assert!(unranked_route_collisions(&["/<foo>?a=b&<c>", "/<foo>?d=e"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_collisions() {
|
||||
assert!(!unranked_route_collisions(&["/<a>", "/a/<a..>"]));
|
||||
|
@ -198,6 +213,20 @@ mod test {
|
|||
assert!(!default_rank_route_collisions(&["/a/b", "/a/b/<c..>"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collision_when_ranked_query() {
|
||||
assert!(default_rank_route_collisions(&["/a?a=b", "/a?c=d"]));
|
||||
assert!(default_rank_route_collisions(&["/<foo>?a=b", "/<foo>?c=d&<d>"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_collision_when_ranked_query() {
|
||||
assert!(!default_rank_route_collisions(&["/", "/?<c..>"]));
|
||||
assert!(!default_rank_route_collisions(&["/hi", "/hi?<c>"]));
|
||||
assert!(!default_rank_route_collisions(&["/hi", "/hi?c"]));
|
||||
assert!(!default_rank_route_collisions(&["/hi?<c>", "/hi?c"]));
|
||||
}
|
||||
|
||||
fn route<'a>(router: &'a Router, method: Method, uri: &str) -> Option<&'a Route> {
|
||||
let rocket = Rocket::custom(Config::development().unwrap());
|
||||
let request = Request::new(&rocket, method, Origin::parse(uri).unwrap());
|
||||
|
@ -293,13 +322,17 @@ mod test {
|
|||
assert_ranked_routes!(&["/<a>/<b>", "/hi/a"], "/hi/c", "/<a>/<b>");
|
||||
assert_ranked_routes!(&["/hi/a", "/hi/<c>"], "/hi/c", "/hi/<c>");
|
||||
assert_ranked_routes!(&["/a", "/a?<b>"], "/a?b=c", "/a?<b>");
|
||||
assert_ranked_routes!(&["/a", "/a?<b>"], "/a", "/a");
|
||||
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/a", "/a");
|
||||
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/b", "/<a>");
|
||||
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"],
|
||||
"/b?v=1", "/<a>?<b>");
|
||||
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"],
|
||||
"/a?b=c", "/a?<b>");
|
||||
assert_ranked_routes!(&["/a", "/a?<b>"], "/a", "/a?<b>");
|
||||
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/a", "/a?<b>");
|
||||
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/b", "/<a>?<b>");
|
||||
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/b?v=1", "/<a>?<b>");
|
||||
assert_ranked_routes!(&["/a", "/<a>", "/a?<b>", "/<a>?<b>"], "/a?b=c", "/a?<b>");
|
||||
assert_ranked_routes!(&["/a", "/a?b"], "/a?b", "/a?b");
|
||||
assert_ranked_routes!(&["/<a>", "/a?b"], "/a?b", "/a?b");
|
||||
assert_ranked_routes!(&["/a", "/<a>?b"], "/a?b", "/a");
|
||||
assert_ranked_routes!(&["/a?<c>&b", "/a?<b>"], "/a", "/a?<b>");
|
||||
assert_ranked_routes!(&["/a?<c>&b", "/a?<b>"], "/a?b", "/a?<c>&b");
|
||||
assert_ranked_routes!(&["/a?<c>&b", "/a?<b>"], "/a?c", "/a?<b>");
|
||||
}
|
||||
|
||||
fn ranked_collisions(routes: &[(isize, &'static str)]) -> bool {
|
||||
|
@ -431,50 +464,13 @@ mod test {
|
|||
assert_default_ranked_routing!(
|
||||
to: "/a/b",
|
||||
with: ["/a/<b>", "/a/b", "/a/b?<v>", "/a/<b>?<v>"],
|
||||
expect: "/a/b", "/a/<b>"
|
||||
expect: "/a/b?<v>", "/a/b", "/a/<b>?<v>", "/a/<b>"
|
||||
);
|
||||
|
||||
assert_default_ranked_routing!(
|
||||
to: "/a/b?c",
|
||||
with: ["/a/b", "/a/b?<c>", "/a/b?c", "/a/<b>?c", "/a/<b>?<c>", "/<a>/<b>"],
|
||||
expect: "/a/b?c", "/a/b?<c>", "/a/b", "/a/<b>?c", "/a/<b>?<c>", "/<a>/<b>"
|
||||
);
|
||||
}
|
||||
|
||||
fn match_params(router: &Router, path: &str, expected: &[&str]) -> bool {
|
||||
println!("Testing: {} (expect: {:?})", path, expected);
|
||||
route(router, Get, path).map_or(false, |route| {
|
||||
let uri = Origin::parse_route(path).unwrap();
|
||||
let params = route.get_param_indexes(&uri);
|
||||
if params.len() != expected.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (k, (i, j)) in params.into_iter().enumerate() {
|
||||
if &path[i..j] != expected[k] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_params() {
|
||||
let router = router_with_routes(&["/<a>"]);
|
||||
assert!(match_params(&router, "/hello", &["hello"]));
|
||||
assert!(match_params(&router, "/hi", &["hi"]));
|
||||
assert!(match_params(&router, "/bob", &["bob"]));
|
||||
assert!(match_params(&router, "/i", &["i"]));
|
||||
|
||||
let router = router_with_routes(&["/hello"]);
|
||||
assert!(match_params(&router, "/hello", &[]));
|
||||
|
||||
let router = router_with_routes(&["/<a>/<b>"]);
|
||||
assert!(match_params(&router, "/a/b", &["a", "b"]));
|
||||
assert!(match_params(&router, "/912/sas", &["912", "sas"]));
|
||||
|
||||
let router = router_with_routes(&["/hello/<b>"]);
|
||||
assert!(match_params(&router, "/hello/b", &["b"]));
|
||||
assert!(match_params(&router, "/hello/sergio", &["sergio"]));
|
||||
|
||||
let router = router_with_routes(&["/hello/<b>/age"]);
|
||||
assert!(match_params(&router, "/hello/sergio/age", &["sergio"]));
|
||||
assert!(match_params(&router, "/hello/you/age", &["you"]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::fmt;
|
||||
use std::fmt::{self, Display};
|
||||
use std::convert::From;
|
||||
|
||||
use yansi::Color::*;
|
||||
|
@ -6,8 +6,9 @@ use yansi::Color::*;
|
|||
use codegen::StaticRouteInfo;
|
||||
use handler::Handler;
|
||||
use http::{Method, MediaType};
|
||||
use http::route::{RouteSegment, Kind, Error as SegmentError};
|
||||
use http::ext::IntoOwned;
|
||||
use http::uri::Origin;
|
||||
use http::uri::{self, Origin};
|
||||
|
||||
/// A route: a method, its handler, path, rank, and format/media type.
|
||||
#[derive(Clone)]
|
||||
|
@ -27,71 +28,124 @@ pub struct Route {
|
|||
pub rank: isize,
|
||||
/// The media type this route matches against, if any.
|
||||
pub format: Option<MediaType>,
|
||||
/// Cached metadata that aids in routing later.
|
||||
crate metadata: Metadata
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
crate struct Metadata {
|
||||
crate path_segments: Vec<RouteSegment<'static>>,
|
||||
crate query_segments: Option<Vec<RouteSegment<'static>>>,
|
||||
crate fully_dynamic_query: bool,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
fn from(route: &Route) -> Result<Metadata, RouteUriError> {
|
||||
let path_segments = RouteSegment::parse_path(&route.uri)
|
||||
.map(|res| res.map(|s| s.into_owned()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let (query_segments, dyn) = match RouteSegment::parse_query(&route.uri) {
|
||||
Some(results) => {
|
||||
let segments = results.map(|res| res.map(|s| s.into_owned()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let dynamic = !segments.iter().any(|s| s.kind == Kind::Static);
|
||||
|
||||
(Some(segments), dynamic)
|
||||
}
|
||||
None => (None, true)
|
||||
};
|
||||
|
||||
Ok(Metadata { path_segments, query_segments, fully_dynamic_query: dyn })
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn default_rank(uri: &Origin) -> isize {
|
||||
// static path, query = -4; static path, no query = -3
|
||||
// dynamic path, query = -2; dynamic path, no query = -1
|
||||
match (!uri.path().contains('<'), uri.query().is_some()) {
|
||||
(true, true) => -4,
|
||||
(true, false) => -3,
|
||||
(false, true) => -2,
|
||||
(false, false) => -1,
|
||||
fn default_rank(route: &Route) -> isize {
|
||||
let static_path = route.metadata.path_segments.iter().all(|s| s.kind == Kind::Static);
|
||||
let partly_static_query = route.uri.query().map(|_| !route.metadata.fully_dynamic_query);
|
||||
match (static_path, partly_static_query) {
|
||||
(true, Some(true)) => -6, // static path, partly static query
|
||||
(true, Some(false)) => -5, // static path, fully dynamic query
|
||||
(true, None) => -4, // static path, no query
|
||||
(false, Some(true)) => -3, // dynamic path, partly static query
|
||||
(false, Some(false)) => -2, // dynamic path, fully dynamic query
|
||||
(false, None) => -1, // dynamic path, no query
|
||||
}
|
||||
}
|
||||
|
||||
fn panic<U: Display, E: Display, T>(uri: U, e: E) -> T {
|
||||
panic!("invalid URI '{}' used to construct route: {}", uri, e)
|
||||
}
|
||||
|
||||
impl Route {
|
||||
/// Creates a new route with the given method, path, and handler with a base
|
||||
/// of `/`.
|
||||
///
|
||||
/// # Ranking
|
||||
///
|
||||
/// The route's rank is set so that routes with static paths are ranked
|
||||
/// higher than routes with dynamic paths, and routes with query strings
|
||||
/// are ranked higher than routes without query strings. This default ranking
|
||||
/// is summarized by the table below:
|
||||
/// The route's rank is set so that routes with static paths (no dynamic
|
||||
/// parameters) are ranked higher than routes with dynamic paths, routes
|
||||
/// with query strings with static segments are ranked higher than routes
|
||||
/// with fully dynamic queries, and routes with queries are ranked higher
|
||||
/// than routes without queries. This default ranking is summarized by the
|
||||
/// table below:
|
||||
///
|
||||
/// | static path | query | rank |
|
||||
/// |-------------|-------|------|
|
||||
/// | yes | yes | -4 |
|
||||
/// | yes | no | -3 |
|
||||
/// | no | yes | -2 |
|
||||
/// | no | no | -1 |
|
||||
/// |-------------|---------------|------|
|
||||
/// | yes | partly static | -6 |
|
||||
/// | yes | fully dynamic | -5 |
|
||||
/// | yes | none | -4 |
|
||||
/// | no | partly static | -3 |
|
||||
/// | no | fully dynamic | -2 |
|
||||
/// | no | none | -1 |
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::{Request, Route, Data};
|
||||
/// use rocket::handler::Outcome;
|
||||
/// use rocket::Route;
|
||||
/// use rocket::http::Method;
|
||||
/// # use rocket::{Request, Data};
|
||||
/// # use rocket::handler::Outcome;
|
||||
/// # fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// # Outcome::from(request, "Hello, world!")
|
||||
/// # }
|
||||
///
|
||||
/// fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// Outcome::from(request, "Hello, world!")
|
||||
/// }
|
||||
/// // this is rank -6 (static path, ~static query)
|
||||
/// let route = Route::new(Method::Get, "/foo?bar=baz&<zoo>", handler);
|
||||
/// assert_eq!(route.rank, -6);
|
||||
///
|
||||
/// // this is a rank -3 route matching requests to `GET /`
|
||||
/// let index = Route::new(Method::Get, "/", handler);
|
||||
/// // this is rank -5 (static path, fully dynamic query)
|
||||
/// let route = Route::new(Method::Get, "/foo?<zoo..>", handler);
|
||||
/// assert_eq!(route.rank, -5);
|
||||
///
|
||||
/// // this is a rank -4 route matching requests to `GET /?<name>`
|
||||
/// let index_name = Route::new(Method::Get, "/?<name>", handler);
|
||||
/// // this is a rank -4 route (static path, no query)
|
||||
/// let route = Route::new(Method::Get, "/", handler);
|
||||
/// assert_eq!(route.rank, -4);
|
||||
///
|
||||
/// // this is a rank -1 route matching requests to `GET /<name>`
|
||||
/// let name = Route::new(Method::Get, "/<name>", handler);
|
||||
/// // this is a rank -3 route (dynamic path, ~static query)
|
||||
/// let route = Route::new(Method::Get, "/foo/<bar>?blue", handler);
|
||||
/// assert_eq!(route.rank, -3);
|
||||
///
|
||||
/// // this is a rank -2 route (dynamic path, fully dynamic query)
|
||||
/// let route = Route::new(Method::Get, "/<bar>?<blue>", handler);
|
||||
/// assert_eq!(route.rank, -2);
|
||||
///
|
||||
/// // this is a rank -1 route (dynamic path, no query)
|
||||
/// let route = Route::new(Method::Get, "/<bar>/foo/<baz..>", handler);
|
||||
/// assert_eq!(route.rank, -1);
|
||||
/// ```
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `path` is not a valid origin URI.
|
||||
/// Panics if `path` is not a valid origin URI or Rocket route URI.
|
||||
pub fn new<S, H>(method: Method, path: S, handler: H) -> Route
|
||||
where S: AsRef<str>, H: Handler + 'static
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let origin = Origin::parse_route(path)
|
||||
.expect("invalid URI used as route path in `Route::new()`");
|
||||
|
||||
let rank = default_rank(&origin);
|
||||
Route::ranked(rank, method, path, handler)
|
||||
let mut route = Route::ranked(0, method, path, handler);
|
||||
route.rank = default_rank(&route);
|
||||
route
|
||||
}
|
||||
|
||||
/// Creates a new route with the given rank, method, path, and handler with
|
||||
|
@ -100,13 +154,13 @@ impl Route {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::{Request, Route, Data};
|
||||
/// use rocket::handler::Outcome;
|
||||
/// use rocket::Route;
|
||||
/// use rocket::http::Method;
|
||||
///
|
||||
/// fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// Outcome::from(request, "Hello, world!")
|
||||
/// }
|
||||
/// # use rocket::{Request, Data};
|
||||
/// # use rocket::handler::Outcome;
|
||||
/// # fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// # Outcome::from(request, "Hello, world!")
|
||||
/// # }
|
||||
///
|
||||
/// // this is a rank 1 route matching requests to `GET /`
|
||||
/// let index = Route::ranked(1, Method::Get, "/", handler);
|
||||
|
@ -114,21 +168,35 @@ impl Route {
|
|||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `path` is not a valid origin URI.
|
||||
/// Panics if `path` is not a valid origin URI or Rocket route URI.
|
||||
pub fn ranked<S, H>(rank: isize, method: Method, path: S, handler: H) -> Route
|
||||
where S: AsRef<str>, H: Handler + 'static
|
||||
{
|
||||
let uri = Origin::parse_route(path.as_ref())
|
||||
.expect("invalid URI used as route path in `Route::ranked()`")
|
||||
let path = path.as_ref();
|
||||
let uri = Origin::parse_route(path)
|
||||
.unwrap_or_else(|e| panic(path, e))
|
||||
.to_normalized()
|
||||
.into_owned();
|
||||
|
||||
Route {
|
||||
let mut route = Route {
|
||||
name: None,
|
||||
format: None,
|
||||
base: Origin::dummy(),
|
||||
handler: Box::new(handler),
|
||||
metadata: Metadata::default(),
|
||||
method, rank, uri
|
||||
};
|
||||
|
||||
route.update_metadata().unwrap_or_else(|e| panic(path, e));
|
||||
route
|
||||
}
|
||||
|
||||
/// Updates the cached routing metadata. MUST be called whenver the route's
|
||||
/// URI is set or changes.
|
||||
fn update_metadata(&mut self) -> Result<(), RouteUriError> {
|
||||
let new_metadata = Metadata::from(&*self)?;
|
||||
self.metadata = new_metadata;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves the path of the base mount point of this route as an `&str`.
|
||||
|
@ -136,15 +204,16 @@ impl Route {
|
|||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::{Request, Route, Data};
|
||||
/// use rocket::handler::Outcome;
|
||||
/// use rocket::Route;
|
||||
/// use rocket::http::Method;
|
||||
/// # use rocket::{Request, Data};
|
||||
/// # use rocket::handler::Outcome;
|
||||
/// #
|
||||
/// # fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// # Outcome::from(request, "Hello, world!")
|
||||
/// # }
|
||||
///
|
||||
/// fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// Outcome::from(request, "Hello, world!")
|
||||
/// }
|
||||
///
|
||||
/// let mut index = Route::ranked(1, Method::Get, "/", handler);
|
||||
/// let mut index = Route::new(Method::Get, "/", handler);
|
||||
/// assert_eq!(index.base(), "/");
|
||||
/// assert_eq!(index.base.path(), "/");
|
||||
/// ```
|
||||
|
@ -153,80 +222,95 @@ impl Route {
|
|||
self.base.path()
|
||||
}
|
||||
|
||||
/// Sets the base mount point of the route. Does not update the rank or any
|
||||
/// other parameters. If `path` contains a query, it is ignored.
|
||||
/// Sets the base mount point of the route to `base` and sets the path to
|
||||
/// `path`. The `path` should _not_ contains the `base` mount point. If
|
||||
/// `base` contains a query, it is ignored. Note that `self.uri` will
|
||||
/// include the new `base` after this method is called.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any of the following occur:
|
||||
///
|
||||
/// * The base mount point contains dynamic parameters.
|
||||
/// * The base mount point or path contain encoded characters.
|
||||
/// * The path is not a valid Rocket route URI.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::{Request, Route, Data};
|
||||
/// use rocket::Route;
|
||||
/// use rocket::http::{Method, uri::Origin};
|
||||
/// use rocket::handler::Outcome;
|
||||
/// # use rocket::{Request, Data};
|
||||
/// # use rocket::handler::Outcome;
|
||||
/// #
|
||||
/// # fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// # Outcome::from(request, "Hello, world!")
|
||||
/// # }
|
||||
///
|
||||
/// fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// Outcome::from(request, "Hello, world!")
|
||||
/// }
|
||||
///
|
||||
/// let mut index = Route::ranked(1, Method::Get, "/", handler);
|
||||
/// let mut index = Route::new(Method::Get, "/", handler);
|
||||
/// assert_eq!(index.base(), "/");
|
||||
/// assert_eq!(index.base.path(), "/");
|
||||
///
|
||||
/// index.set_base(Origin::parse("/hi").unwrap());
|
||||
/// assert_eq!(index.base(), "/hi");
|
||||
/// assert_eq!(index.base.path(), "/hi");
|
||||
/// let new_base = Origin::parse("/greeting").unwrap();
|
||||
/// let new_uri = Origin::parse("/hi").unwrap();
|
||||
/// index.set_uri(new_base, new_uri);
|
||||
/// assert_eq!(index.base(), "/greeting");
|
||||
/// assert_eq!(index.uri.path(), "/greeting/hi");
|
||||
/// ```
|
||||
pub fn set_base<'a>(&mut self, path: Origin<'a>) {
|
||||
self.base = path.into_owned();
|
||||
self.base.clear_query();
|
||||
}
|
||||
|
||||
/// Sets the path of the route. Does not update the rank or any other
|
||||
/// parameters.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rocket::{Request, Route, Data};
|
||||
/// use rocket::http::{Method, uri::Origin};
|
||||
/// use rocket::handler::Outcome;
|
||||
///
|
||||
/// fn handler<'r>(request: &'r Request, _data: Data) -> Outcome<'r> {
|
||||
/// Outcome::from(request, "Hello, world!")
|
||||
/// }
|
||||
///
|
||||
/// let mut index = Route::ranked(1, Method::Get, "/", handler);
|
||||
/// assert_eq!(index.uri.path(), "/");
|
||||
///
|
||||
/// index.set_uri(Origin::parse("/hello").unwrap());
|
||||
/// assert_eq!(index.uri.path(), "/hello");
|
||||
/// ```
|
||||
pub fn set_uri<'a>(&mut self, uri: Origin<'a>) {
|
||||
self.uri = uri.into_owned();
|
||||
}
|
||||
|
||||
// FIXME: Decide whether a component has to be fully variable or not. That
|
||||
// is, whether you can have: /a<a>b/ or even /<a>:<b>/
|
||||
// TODO: Don't return a Vec...take in an &mut [&'a str] (no alloc!)
|
||||
/// Given a URI, returns a vector of slices of that URI corresponding to the
|
||||
/// dynamic segments in this route.
|
||||
crate fn get_param_indexes(&self, uri: &Origin) -> Vec<(usize, usize)> {
|
||||
let route_segs = self.uri.segments();
|
||||
let uri_segs = uri.segments();
|
||||
let start_addr = uri.path().as_ptr() as usize;
|
||||
|
||||
let mut result = Vec::with_capacity(self.uri.segment_count());
|
||||
for (route_seg, uri_seg) in route_segs.zip(uri_segs) {
|
||||
let i = (uri_seg.as_ptr() as usize) - start_addr;
|
||||
if route_seg.ends_with("..>") {
|
||||
result.push((i, uri.path().len()));
|
||||
break;
|
||||
} else if route_seg.ends_with('>') {
|
||||
let j = i + uri_seg.len();
|
||||
result.push((i, j));
|
||||
pub fn set_uri<'a>(
|
||||
&mut self,
|
||||
mut base: Origin<'a>,
|
||||
path: Origin<'a>
|
||||
) -> Result<(), RouteUriError> {
|
||||
base.clear_query();
|
||||
for segment in RouteSegment::parse_path(&base) {
|
||||
if segment?.kind != Kind::Static {
|
||||
return Err(RouteUriError::DynamicBase);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
let complete_uri = format!("{}/{}", base, path);
|
||||
let uri = Origin::parse_route(&complete_uri)?;
|
||||
self.base = base.to_normalized().into_owned();
|
||||
self.uri = uri.to_normalized().into_owned();
|
||||
self.update_metadata()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RouteUriError {
|
||||
Segment,
|
||||
Uri(uri::Error<'static>),
|
||||
DynamicBase,
|
||||
}
|
||||
|
||||
impl<'a> From<(&'a str, SegmentError<'a>)> for RouteUriError {
|
||||
fn from(_: (&'a str, SegmentError<'a>)) -> Self {
|
||||
RouteUriError::Segment
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<uri::Error<'a>> for RouteUriError {
|
||||
fn from(error: uri::Error<'a>) -> Self {
|
||||
RouteUriError::Uri(error.into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RouteUriError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
RouteUriError::Segment => {
|
||||
write!(f, "The URI contains malformed dynamic route path segments.")
|
||||
}
|
||||
RouteUriError::DynamicBase => {
|
||||
write!(f, "The mount point contains dynamic parameters.")
|
||||
}
|
||||
RouteUriError::Uri(error) => {
|
||||
write!(f, "Malformed URI: {}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,6 +344,7 @@ impl fmt::Debug for Route {
|
|||
.field("uri", &self.uri)
|
||||
.field("rank", &self.rank)
|
||||
.field("format", &self.format)
|
||||
.field("metadata", &self.metadata)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct Query {
|
||||
field: String
|
||||
}
|
||||
|
||||
#[get("/?<query>")]
|
||||
fn first(query: Query) -> String {
|
||||
query.field
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
fn second() -> &'static str {
|
||||
"no query"
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rocket::Rocket;
|
||||
use rocket::local::Client;
|
||||
|
||||
fn assert_no_collision(rocket: Rocket) {
|
||||
let client = Client::new(rocket).unwrap();
|
||||
let mut response = client.get("/?field=query").dispatch();
|
||||
assert_eq!(response.body_string(), Some("query".into()));
|
||||
|
||||
let mut response = client.get("/").dispatch();
|
||||
assert_eq!(response.body_string(), Some("no query".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_query_collisions() {
|
||||
let rocket = rocket::ignite().mount("/", routes![first, second]);
|
||||
assert_no_collision(rocket);
|
||||
|
||||
let rocket = rocket::ignite().mount("/", routes![second, first]);
|
||||
assert_no_collision(rocket);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
#![feature(plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
#![allow(dead_code)] // This test is only here so that we can ensure it compiles.
|
||||
|
||||
extern crate rocket;
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
use rocket::State;
|
||||
use rocket::response::{self, Responder};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#![feature(plugin, decl_macro, proc_macro_non_items)]
|
||||
#![plugin(rocket_codegen)]
|
||||
#![feature(proc_macro_non_items, proc_macro_gen, decl_macro)]
|
||||
|
||||
#[macro_use] extern crate rocket;
|
||||
|
||||
|
|
|
@ -6,4 +6,3 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
rocket = { path = "../../core/lib" }
|
||||
rocket_codegen = { path = "../../core/codegen" }
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#![feature(plugin, decl_macro)]
|
||||
#![plugin(rocket_codegen)]
|
||||
|
||||
extern crate rocket;
|
||||
|
||||
// This example's illustration is the Rocket.toml file.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue