Provide attribute macro for multiple HTTP methods

What
--
Define a new `route` attribute macro that supports defining multiple
HTTP methods to routed to (handled by) a single handler.

The attribute macro syntax looks like this
```rust
use actix_web::route;

async fn multi_methods() -> &'static str {
    "Hello world!\r\n"
}
```

How
--
This implementation extends the [`GuardType`][1] enum in actix-web-codegen to have a
new `GuardType::Multi` variant that denotes when multiple method guards
are used.

A new `methods` attribute in the `route` attribute macro provides a
comma-separated list of HTTP methods to provide guard for.

The code parses the methods list, matches them to the respective
`GuardType` and uses the `AnyGuard` struct to combine them together.

A constructor method for [`AnyGuard`][2] is added to support this.

The generated code looks like this:
```rust
pub struct multi_methods;
impl actix_web::dev::HttpServiceFactory for multi_methods {
    fn register(self, __config: &mut actix_web::dev::AppService) {
    ¦   async fn multi_methods() -> &'static str {
    ¦   ¦   "Hello world!\r\n"
    ¦   }
    ¦   let __resource = actix_web::Resource::new("/multi")
    ¦   ¦   .name("multi_methods")
    ¦   ¦   .guard(actix_web:💂:AnyGuard::new(<[_]>::into_vec(box [
    ¦   ¦   ¦   Box::new(actix_web:💂:Get()),
    ¦   ¦   ¦   Box::new(actix_web:💂:Post()),
    ¦   ¦   ])))
    ¦   ¦   .to(multi_methods);
    ¦   actix_web::dev::HttpServiceFactory::register(__resource, __config)
    }
}
```

**NOTE: This is my first attempt that implementing this feature.
Feedback and mentorship is highly welcome to improve it :-)**

Why
--
This fixes https://github.com/actix/actix-web/issues/1360

[1]: https://github.com/actix/actix-web/blob/master/actix-web-codegen/src/route.rs#L21
[2]: https://github.com/actix/actix-web/blob/master/src/guard.rs#L104s
This commit is contained in:
Matt Gathu 2020-09-12 15:01:03 +02:00
parent d707704556
commit 31348a2339
3 changed files with 96 additions and 14 deletions

View File

@ -141,6 +141,20 @@ pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Patch)
}
/// Creates route handler with Multiple HTTP methods guards.
///
/// Syntax: `#[route("path"[, attributes])]`
///
/// ## Attributes
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
/// - `methods="HTTP_METHOD_1,HTTP_METHOD_2"` - Registers HTTP methods to provide guards for.
/// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
/// - `wrap="Middleware"` - Registers a resource middleware.
#[proc_macro_attribute]
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Multi)
}
/// Marks async main function as the actix system entry-point.
///
/// ## Usage

View File

@ -17,7 +17,7 @@ impl ToTokens for ResourceType {
}
}
#[derive(PartialEq)]
#[derive(Debug, PartialEq)]
pub enum GuardType {
Get,
Post,
@ -28,6 +28,7 @@ pub enum GuardType {
Options,
Trace,
Patch,
Multi,
}
impl GuardType {
@ -42,6 +43,7 @@ impl GuardType {
GuardType::Options => "Options",
GuardType::Trace => "Trace",
GuardType::Patch => "Patch",
GuardType::Multi => "Multi",
}
}
}
@ -57,6 +59,7 @@ struct Args {
path: syn::LitStr,
guards: Vec<Ident>,
wrappers: Vec<syn::Type>,
methods: Vec<GuardType>,
}
impl Args {
@ -64,6 +67,7 @@ impl Args {
let mut path = None;
let mut guards = Vec::new();
let mut wrappers = Vec::new();
let mut methods = Vec::new();
for arg in args {
match arg {
NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
@ -96,6 +100,36 @@ impl Args {
"Attribute wrap expects type",
));
}
} else if nv.path.is_ident("methods") {
if let syn::Lit::Str(ref lit) = nv.lit {
for meth in lit.value().split(',') {
match meth.to_uppercase().as_str() {
"CONNECT" => methods.push(GuardType::Connect),
"DELETE" => methods.push(GuardType::Delete),
"GET" => methods.push(GuardType::Get),
"HEAD" => methods.push(GuardType::Head),
"OPTIONS" => methods.push(GuardType::Options),
"PATCH" => methods.push(GuardType::Patch),
"POST" => methods.push(GuardType::Post),
"PUT" => methods.push(GuardType::Put),
"TRACE" => methods.push(GuardType::Trace),
_ => {
return Err(syn::Error::new_spanned(
nv.lit,
&format!(
"Unexpected HTTP Method: `{}`",
meth
),
))
}
};
}
} else {
return Err(syn::Error::new_spanned(
nv.lit,
"Attribute methods expects literal string!",
));
}
} else {
return Err(syn::Error::new_spanned(
nv.path,
@ -112,6 +146,7 @@ impl Args {
path: path.unwrap(),
guards,
wrappers,
methods,
})
}
}
@ -166,6 +201,13 @@ impl Route {
let args = Args::new(args)?;
if guard == GuardType::Multi && args.methods.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
"The #[route(..)] macro requires the `methods` attribute!",
));
}
let resource_type = if ast.sig.asyncness.is_some() {
ResourceType::Async
} else {
@ -201,11 +243,13 @@ impl ToTokens for Route {
path,
guards,
wrappers,
methods,
},
resource_type,
} = self;
let resource_name = name.to_string();
let stream = quote! {
let stream = if guard != &GuardType::Multi {
quote! {
#[allow(non_camel_case_types, missing_docs)]
pub struct #name;
@ -222,6 +266,26 @@ impl ToTokens for Route {
actix_web::dev::HttpServiceFactory::register(__resource, __config)
}
}
}
} else {
quote! {
#[allow(non_camel_case_types, missing_docs)]
pub struct #name;
impl actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) {
#ast
let __resource = actix_web::Resource::new(#path)
.name(#resource_name)
.guard(actix_web::guard::AnyGuard::new(vec![#(Box::new(actix_web::guard::#methods())),*]))
#(.guard(actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
actix_web::dev::HttpServiceFactory::register(__resource, __config)
}
}
}
};
output.extend(stream);

View File

@ -104,6 +104,10 @@ pub fn Any<F: Guard + 'static>(guard: F) -> AnyGuard {
pub struct AnyGuard(Vec<Box<dyn Guard>>);
impl AnyGuard {
/// Create AnyGuard from a vector of Guards
pub fn new(guards: Vec<Box<dyn Guard>>) -> Self {
AnyGuard(guards)
}
/// Add guard to a list of guards to check
pub fn or<F: Guard + 'static>(mut self, guard: F) -> Self {
self.0.push(Box::new(guard));