Generate a proxy structure for better namespacing.

Prior to this commit, it was impossible to 'use' a route from a separate
namespace for use in a 'routes!' macro. Naturally, this was a common
source of confusion amongst users. This commit obviates this deficiency
by generating a "proxy" structure that can be imported and converted
into a 'Route'/'Catcher' or their static variants.

This change is largely backwards compatible but can break existing code
when routes are named identically to other types in the namespace.
This commit is contained in:
Sergio Benitez 2020-10-12 22:11:44 -07:00
parent 29f4726127
commit 092e03f720
11 changed files with 138 additions and 151 deletions

View File

@ -3,9 +3,8 @@ use devise::{syn, MetaItem, Spanned, Result, FromMeta, Diagnostic};
use crate::http_codegen::{self, Optional};
use crate::proc_macro2::{TokenStream, Span};
use crate::syn_ext::{IdentExt, ReturnTypeExt, TokenStreamExt};
use crate::syn_ext::{ReturnTypeExt, TokenStreamExt};
use self::syn::{Attribute, parse::Parser};
use crate::{CATCH_FN_PREFIX, CATCH_STRUCT_PREFIX};
/// The raw, parsed `#[catch(code)]` attribute.
#[derive(Debug, FromMeta)]
@ -75,14 +74,13 @@ pub fn _catch(
// Gather everything we'll need to generate the catcher.
let user_catcher_fn = &catch.function;
let user_catcher_fn_name = catch.function.sig.ident.clone();
let generated_struct_name = user_catcher_fn_name.prepend(CATCH_STRUCT_PREFIX);
let generated_fn_name = user_catcher_fn_name.prepend(CATCH_FN_PREFIX);
let (vis, catcher_status) = (&catch.function.vis, &catch.status);
let status_code = Optional(catcher_status.as_ref().map(|s| s.0.code));
// Variables names we'll use and reuse.
define_vars_and_mods!(catch.function.span().into() =>
req, status, _Box, Request, Response, ErrorHandlerFuture, Status);
req, status, _Box, Request, Response, StaticCatcherInfo, Catcher,
ErrorHandlerFuture, Status);
// Determine the number of parameters that will be passed in.
if catch.function.sig.inputs.len() > 2 {
@ -119,29 +117,41 @@ pub fn _catch(
Ok(quote! {
#user_catcher_fn
/// Rocket code generated wrapping catch function.
#[doc(hidden)]
#vis fn #generated_fn_name<'_b>(
#status: #Status,
#req: &'_b #Request
) -> #ErrorHandlerFuture<'_b> {
#_Box::pin(async move {
let __response = #catcher_response;
#Response::build()
.status(#status)
.merge(__response)
.ok()
})
#[allow(non_camel_case_types)]
/// Rocket code generated proxy structure.
#vis struct #user_catcher_fn_name { }
/// Rocket code generated proxy static conversion implementation.
impl From<#user_catcher_fn_name> for #StaticCatcherInfo {
fn from(_: #user_catcher_fn_name) -> #StaticCatcherInfo {
fn monomorphized_function<'_b>(
#status: #Status,
#req: &'_b #Request
) -> #ErrorHandlerFuture<'_b> {
#_Box::pin(async move {
let __response = #catcher_response;
#Response::build()
.status(#status)
.merge(__response)
.ok()
})
}
#StaticCatcherInfo {
code: #status_code,
handler: monomorphized_function,
}
}
}
/// Rocket code generated static catcher info.
#[doc(hidden)]
#[allow(non_upper_case_globals)]
#vis static #generated_struct_name: ::rocket::StaticCatcherInfo =
::rocket::StaticCatcherInfo {
code: #status_code,
handler: #generated_fn_name,
};
/// Rocket code generated proxy conversion implementation.
impl From<#user_catcher_fn_name> for #Catcher {
#[inline]
fn from(_: #user_catcher_fn_name) -> #Catcher {
#StaticCatcherInfo::from(#user_catcher_fn_name {}).into()
}
}
})
}

View File

@ -13,7 +13,7 @@ use crate::http_codegen::{Method, MediaType, RoutePath, DataSegment, Optional};
use crate::attribute::segments::{Source, Kind, Segment};
use crate::syn::{Attribute, parse::Parser};
use crate::{ROUTE_FN_PREFIX, ROUTE_STRUCT_PREFIX, URI_MACRO_PREFIX, ROCKET_PARAM_PREFIX};
use crate::{URI_MACRO_PREFIX, ROCKET_PARAM_PREFIX};
/// The raw, parsed `#[route]` attribute.
#[derive(Debug, FromMeta)]
@ -408,11 +408,9 @@ fn codegen_route(route: Route) -> Result<TokenStream> {
}
// Gather everything we need.
define_vars_and_mods!(req, data, _Box, Request, Data, StaticRouteInfo, HandlerFuture);
define_vars_and_mods!(req, data, _Box, Request, Data, Route, StaticRouteInfo, HandlerFuture);
let (vis, user_handler_fn) = (&route.function.vis, &route.function);
let user_handler_fn_name = &user_handler_fn.sig.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 generated_internal_uri_macro = generate_internal_uri_macro(&route);
let generated_respond_expr = generate_respond_expr(&route);
@ -424,36 +422,48 @@ fn codegen_route(route: Route) -> Result<TokenStream> {
Ok(quote! {
#user_handler_fn
/// Rocket code generated wrapping route function.
#[doc(hidden)]
#vis fn #generated_fn_name<'_b>(
#req: &'_b #Request,
#data: #Data
) -> #HandlerFuture<'_b> {
#_Box::pin(async move {
#(#req_guard_definitions)*
#(#parameter_definitions)*
#data_stmt
#[allow(non_camel_case_types)]
/// Rocket code generated proxy structure.
#vis struct #user_handler_fn_name { }
#generated_respond_expr
})
/// Rocket code generated proxy static conversion implementation.
impl From<#user_handler_fn_name> for #StaticRouteInfo {
fn from(_: #user_handler_fn_name) -> #StaticRouteInfo {
fn monomorphized_function<'_b>(
#req: &'_b #Request,
#data: #Data
) -> #HandlerFuture<'_b> {
#_Box::pin(async move {
#(#req_guard_definitions)*
#(#parameter_definitions)*
#data_stmt
#generated_respond_expr
})
}
#StaticRouteInfo {
name: stringify!(#user_handler_fn_name),
method: #method,
path: #path,
handler: monomorphized_function,
format: #format,
rank: #rank,
}
}
}
/// Rocket code generated proxy conversion implementation.
impl From<#user_handler_fn_name> for #Route {
#[inline]
fn from(_: #user_handler_fn_name) -> #Route {
#StaticRouteInfo::from(#user_handler_fn_name {}).into()
}
}
/// Rocket code generated wrapping URI macro.
#generated_internal_uri_macro
/// Rocket code generated static route info.
#[doc(hidden)]
#[allow(non_upper_case_globals)]
#vis static #generated_struct_name: #StaticRouteInfo =
#StaticRouteInfo {
name: stringify!(#user_handler_fn_name),
method: #method,
path: #path,
handler: #generated_fn_name,
format: #format,
rank: #rank,
};
}.into())
}

View File

@ -1,65 +1,42 @@
use devise::Result;
use crate::syn_ext::IdentExt;
use crate::syn::{Path, punctuated::Punctuated, parse::Parser, Token};
use crate::syn::spanned::Spanned;
use crate::proc_macro2::TokenStream;
use crate::{ROUTE_STRUCT_PREFIX, CATCH_STRUCT_PREFIX};
use crate::syn::spanned::Spanned;
mod uri;
mod uri_parsing;
mod test_guide;
pub fn prefix_last_segment(path: &mut Path, prefix: &str) {
let mut last_seg = path.segments.last_mut().expect("syn::Path has segments");
last_seg.ident = last_seg.ident.prepend(prefix);
}
fn _prefixed_vec(
prefix: &str,
fn struct_maker_vec(
input: proc_macro::TokenStream,
ty: &TokenStream
ty: TokenStream,
) -> Result<TokenStream> {
// Parse a comma-separated list of paths.
let mut paths = <Punctuated<Path, Token![,]>>::parse_terminated.parse(input)?;
// Prefix the last segment in each path with `prefix`.
paths.iter_mut().for_each(|p| prefix_last_segment(p, prefix));
// Return a `vec!` of the prefixed, mapped paths.
let prefixed_mapped_paths = paths.iter()
.map(|path| quote_spanned!(path.span().into() => #ty::from(&#path)));
Ok(quote!(vec![#(#prefixed_mapped_paths),*]))
}
fn prefixed_vec(
prefix: &str,
input: proc_macro::TokenStream,
ty: TokenStream
) -> TokenStream {
define_vars_and_mods!(_Vec);
_prefixed_vec(prefix, input, &ty)
.map(|vec| quote!({
let __vector: #_Vec<#ty> = #vec;
__vector
}))
.unwrap_or_else(|diag| {
let diag_tokens = diag.emit_as_expr_tokens();
quote!({
#diag_tokens
let __vec: #_Vec<#ty> = vec![];
__vec
})
})
// Parse a comma-separated list of paths.
let paths = <Punctuated<Path, Token![,]>>::parse_terminated.parse(input)?;
let exprs = paths.iter()
.map(|path| quote_spanned!(path.span() => {
let ___struct = #path {};
let ___item: #ty = ___struct.into();
___item
}));
Ok(quote!({
let ___vec: #_Vec<#ty> = vec![#(#exprs),*];
___vec
}))
}
pub fn routes_macro(input: proc_macro::TokenStream) -> TokenStream {
prefixed_vec(ROUTE_STRUCT_PREFIX, input, quote!(::rocket::Route))
struct_maker_vec(input, quote!(::rocket::Route))
.unwrap_or_else(|diag| diag.emit_as_expr_tokens())
}
pub fn catchers_macro(input: proc_macro::TokenStream) -> TokenStream {
prefixed_vec(CATCH_STRUCT_PREFIX, input, quote!(::rocket::Catcher))
struct_maker_vec(input, quote!(::rocket::Catcher))
.unwrap_or_else(|diag| diag.emit_as_expr_tokens())
}
pub fn uri_macro(input: proc_macro::TokenStream) -> TokenStream {

View File

@ -8,7 +8,7 @@ use crate::http::route::{RouteSegment, Kind, Source};
use crate::syn::{Expr, Ident, Type, spanned::Spanned};
use crate::http_codegen::Optional;
use crate::syn_ext::IdentExt;
use crate::bang::{prefix_last_segment, uri_parsing::*};
use crate::bang::uri_parsing::*;
use crate::proc_macro2::TokenStream;
use crate::URI_MACRO_PREFIX;
@ -23,6 +23,11 @@ macro_rules! p {
($n:expr, "parameter") => (p!(@go $n, "1 parameter", format!("{} parameters", $n)));
}
pub fn prefix_last_segment(path: &mut syn::Path, prefix: &str) {
let mut last_seg = path.segments.last_mut().expect("syn::Path has segments");
last_seg.ident = last_seg.ident.prepend(prefix);
}
pub fn _uri_macro(input: TokenStream) -> Result<TokenStream> {
let input2: TokenStream = input.clone().into();
let mut params = syn::parse2::<UriParams>(input)?;

View File

@ -92,6 +92,9 @@ vars_and_mods! {
Response => rocket::response::Response,
Data => rocket::data::Data,
StaticRouteInfo => rocket::StaticRouteInfo,
StaticCatcherInfo => rocket::StaticCatcherInfo,
Route => rocket::Route,
Catcher => rocket::Catcher,
SmallVec => rocket::http::private::SmallVec,
Status => rocket::http::Status,
HandlerFuture => rocket::handler::HandlerFuture,
@ -123,10 +126,6 @@ use crate::http::Method;
use proc_macro::TokenStream;
use devise::{proc_macro2, syn};
static ROUTE_STRUCT_PREFIX: &str = "static_rocket_route_info_for_";
static CATCH_STRUCT_PREFIX: &str = "static_rocket_catch_info_for_";
static CATCH_FN_PREFIX: &str = "rocket_catch_fn_";
static ROUTE_FN_PREFIX: &str = "rocket_route_fn_";
static URI_MACRO_PREFIX: &str = "rocket_uri_macro_";
static ROCKET_PARAM_PREFIX: &str = "__rocket_param_";

View File

@ -118,3 +118,23 @@ fn test_full_route() {
assert_eq!(response.into_string().unwrap(), format!("({}, {}, {}, {}, {}, {}) ({})",
sky, name, "A A", "inside", path, simple, expected_uri));
}
mod scopes {
mod other {
#[get("/world")]
pub fn world() -> &'static str {
"Hello, world!"
}
}
#[get("/hello")]
pub fn hello() -> &'static str {
"Hello, outside world!"
}
use other::world;
fn _rocket() -> rocket::Rocket {
rocket::ignite().mount("/", rocket::routes![hello, world])
}
}

View File

@ -5,12 +5,18 @@ use smallvec::{Array, SmallVec};
// TODO: It would be nice if we could somehow have one trait that could give us
// either SmallVec or Vec.
/// Trait implemented by types that can be converted into a collection.
pub trait IntoCollection<T> {
pub trait IntoCollection<T>: Sized {
/// Converts `self` into a collection.
fn into_collection<A: Array<Item=T>>(self) -> SmallVec<A>;
#[doc(hidden)]
fn mapped<U, F: FnMut(T) -> U, A: Array<Item=U>>(self, f: F) -> SmallVec<A>;
#[doc(hidden)]
fn mapped_vec<U, F: FnMut(T) -> U>(self, f: F) -> Vec<U> {
let small = self.mapped::<U, F, [U; 0]>(f);
small.into_vec()
}
}
impl<T> IntoCollection<T> for T {

View File

@ -259,8 +259,9 @@ impl<F: Clone + Sync + Send + 'static> ErrorHandler for F
}
#[doc(hidden)]
impl<'a> From<&'a StaticCatcherInfo> for Catcher {
fn from(info: &'a StaticCatcherInfo) -> Catcher {
impl From<StaticCatcherInfo> for Catcher {
#[inline]
fn from(info: StaticCatcherInfo) -> Catcher {
Catcher::new(info.code, info.handler)
}
}

View File

@ -317,11 +317,11 @@ impl fmt::Debug for Route {
}
#[doc(hidden)]
impl From<&StaticRouteInfo> for Route {
fn from(info: &StaticRouteInfo) -> Route {
impl From<StaticRouteInfo> for Route {
fn from(info: StaticRouteInfo) -> Route {
// This should never panic since `info.path` is statically checked.
let mut route = Route::new(info.method, info.path, info.handler);
route.format = info.format.clone();
route.format = info.format;
route.name = Some(info.name);
if let Some(rank) = info.rank {
route.rank = rank;

View File

@ -8,7 +8,7 @@ fn google() -> Redirect {
}
#[get("/rocket")]
fn rocket() -> Redirect {
fn redirect() -> Redirect {
Redirect::to("https://rocket.rs:80")
}
@ -18,7 +18,7 @@ mod test_absolute_uris_okay {
#[test]
fn redirect_works() {
let rocket = rocket::ignite().mount("/", routes![google, rocket]);
let rocket = rocket::ignite().mount("/", routes![google, redirect]);
let client = Client::new(rocket).unwrap();
let response = client.get("/google").dispatch();

View File

@ -119,47 +119,6 @@ requests to `"/hello/world"` will be directed to the `world` function.
! note: In many cases, the base path will simply be `"/"`.
### Namespacing
When a route is declared inside a module other than the root, you may find
yourself with unexpected errors when mounting:
```rust,compile_fail
# #[macro_use] extern crate rocket;
mod other {
#[get("/world")]
pub fn world() -> &'static str {
"Hello, world!"
}
}
#[get("/hello")]
pub fn hello() -> &'static str {
"Hello, outside world!"
}
use other::world;
fn main() {
// error[E0425]: cannot find value `static_rocket_route_info_for_world` in this scope
rocket::ignite().mount("/hello", routes![hello, world]);
}
```
This occurs because the `routes!` macro implicitly converts the route's name
into the name of a structure generated by Rocket's code generation. The solution
is to refer to the route using a namespaced path instead:
```rust
# #[macro_use] extern crate rocket;
# #[get("/")] pub fn hello() {}
# mod other { #[get("/world")] pub fn world() {} }
rocket::ignite().mount("/hello", routes![hello, other::world]);
```
## Launching
Now that Rocket knows about the route, you can tell Rocket to start accepting