From 31348a23392fc9f5ac60593d6f231f865f0a4e75 Mon Sep 17 00:00:00 2001 From: Matt Gathu Date: Sat, 12 Sep 2020 15:01:03 +0200 Subject: [PATCH] Provide attribute macro for multiple HTTP methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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::guard::AnyGuard::new(<[_]>::into_vec(box [ ¦ ¦ ¦ Box::new(actix_web::guard::Get()), ¦ ¦ ¦ Box::new(actix_web::guard::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 --- actix-web-codegen/src/lib.rs | 14 ++++++ actix-web-codegen/src/route.rs | 92 ++++++++++++++++++++++++++++------ src/guard.rs | 4 ++ 3 files changed, 96 insertions(+), 14 deletions(-) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 445fe924d..9b71f2752 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -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 diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 676e75e07..ef5c1ee3d 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -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, wrappers: Vec, + methods: Vec, } 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,25 +243,47 @@ impl ToTokens for Route { path, guards, wrappers, + methods, }, resource_type, } = self; let resource_name = name.to_string(); - let stream = quote! { - #[allow(non_camel_case_types, missing_docs)] - pub struct #name; + let stream = if guard != &GuardType::Multi { + 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::#guard()) - #(.guard(actix_web::guard::fn_guard(#guards)))* - #(.wrap(#wrappers))* - .#resource_type(#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::#guard()) + #(.guard(actix_web::guard::fn_guard(#guards)))* + #(.wrap(#wrappers))* + .#resource_type(#name); - actix_web::dev::HttpServiceFactory::register(__resource, __config) + 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) + } } } }; diff --git a/src/guard.rs b/src/guard.rs index 25284236a..00f05999f 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -104,6 +104,10 @@ pub fn Any(guard: F) -> AnyGuard { pub struct AnyGuard(Vec>); impl AnyGuard { + /// Create AnyGuard from a vector of Guards + pub fn new(guards: Vec>) -> Self { + AnyGuard(guards) + } /// Add guard to a list of guards to check pub fn or(mut self, guard: F) -> Self { self.0.push(Box::new(guard));