1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-30 10:42:55 +01:00

Provide attribute macro for multiple HTTP methods (#1674)

Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
Matt Gathu 2020-09-16 23:37:41 +02:00 committed by GitHub
parent d707704556
commit 509b2e6eec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 263 additions and 4 deletions

View File

@ -2,8 +2,10 @@
## Unreleased - 2020-xx-xx ## Unreleased - 2020-xx-xx
* Added compile success and failure testing. [#1677] * Added compile success and failure testing. [#1677]
* Add `route` macro for supporting multiple HTTP methods guards.
[#1677]: https://github.com/actix/actix-web/pull/1677 [#1677]: https://github.com/actix/actix-web/pull/1677
[#1674]: https://github.com/actix/actix-web/pull/1674
## 0.3.0 - 2020-09-11 ## 0.3.0 - 2020-09-11

View File

@ -23,3 +23,4 @@ actix-rt = "1.0.0"
actix-web = "3.0.0" actix-web = "3.0.0"
futures-util = { version = "0.3.5", default-features = false } futures-util = { version = "0.3.5", default-features = false }
trybuild = "1" trybuild = "1"
rustversion = "1"

View File

@ -141,6 +141,23 @@ pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Patch) route::generate(args, input, route::GuardType::Patch)
} }
/// Creates resource handler, allowing multiple HTTP method guards.
///
/// Syntax: `#[route("path"[, attributes])]`
///
/// Example: `#[route("/", method="GET", method="HEAD")]`
///
/// ## Attributes
///
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard 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. /// Marks async main function as the actix system entry-point.
/// ///
/// ## Usage /// ## Usage

View File

@ -1,5 +1,8 @@
extern crate proc_macro; extern crate proc_macro;
use std::collections::HashSet;
use std::convert::TryFrom;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens, TokenStreamExt}; use quote::{format_ident, quote, ToTokens, TokenStreamExt};
@ -17,7 +20,7 @@ impl ToTokens for ResourceType {
} }
} }
#[derive(PartialEq)] #[derive(Debug, PartialEq, Eq, Hash)]
pub enum GuardType { pub enum GuardType {
Get, Get,
Post, Post,
@ -28,6 +31,7 @@ pub enum GuardType {
Options, Options,
Trace, Trace,
Patch, Patch,
Multi,
} }
impl GuardType { impl GuardType {
@ -42,6 +46,7 @@ impl GuardType {
GuardType::Options => "Options", GuardType::Options => "Options",
GuardType::Trace => "Trace", GuardType::Trace => "Trace",
GuardType::Patch => "Patch", GuardType::Patch => "Patch",
GuardType::Multi => "Multi",
} }
} }
} }
@ -53,10 +58,33 @@ impl ToTokens for GuardType {
} }
} }
impl TryFrom<&syn::LitStr> for GuardType {
type Error = syn::Error;
fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
match value.value().as_str() {
"CONNECT" => Ok(GuardType::Connect),
"DELETE" => Ok(GuardType::Delete),
"GET" => Ok(GuardType::Get),
"HEAD" => Ok(GuardType::Head),
"OPTIONS" => Ok(GuardType::Options),
"PATCH" => Ok(GuardType::Patch),
"POST" => Ok(GuardType::Post),
"PUT" => Ok(GuardType::Put),
"TRACE" => Ok(GuardType::Trace),
_ => Err(syn::Error::new_spanned(
value,
&format!("Unexpected HTTP Method: `{}`", value.value()),
)),
}
}
}
struct Args { struct Args {
path: syn::LitStr, path: syn::LitStr,
guards: Vec<Ident>, guards: Vec<Ident>,
wrappers: Vec<syn::Type>, wrappers: Vec<syn::Type>,
methods: HashSet<GuardType>,
} }
impl Args { impl Args {
@ -64,6 +92,7 @@ impl Args {
let mut path = None; let mut path = None;
let mut guards = Vec::new(); let mut guards = Vec::new();
let mut wrappers = Vec::new(); let mut wrappers = Vec::new();
let mut methods = HashSet::new();
for arg in args { for arg in args {
match arg { match arg {
NestedMeta::Lit(syn::Lit::Str(lit)) => match path { NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
@ -96,10 +125,28 @@ impl Args {
"Attribute wrap expects type", "Attribute wrap expects type",
)); ));
} }
} else if nv.path.is_ident("method") {
if let syn::Lit::Str(ref lit) = nv.lit {
let guard = GuardType::try_from(lit)?;
if !methods.insert(guard) {
return Err(syn::Error::new_spanned(
&nv.lit,
&format!(
"HTTP Method defined more than once: `{}`",
lit.value()
),
));
}
} else {
return Err(syn::Error::new_spanned(
nv.lit,
"Attribute method expects literal string!",
));
}
} else { } else {
return Err(syn::Error::new_spanned( return Err(syn::Error::new_spanned(
nv.path, nv.path,
"Unknown attribute key is specified. Allowed: guard and wrap", "Unknown attribute key is specified. Allowed: guard, method and wrap",
)); ));
} }
} }
@ -112,6 +159,7 @@ impl Args {
path: path.unwrap(), path: path.unwrap(),
guards, guards,
wrappers, wrappers,
methods,
}) })
} }
} }
@ -166,6 +214,13 @@ impl Route {
let args = Args::new(args)?; 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 at least one `method` attribute",
));
}
let resource_type = if ast.sig.asyncness.is_some() { let resource_type = if ast.sig.asyncness.is_some() {
ResourceType::Async ResourceType::Async
} else { } else {
@ -201,10 +256,29 @@ impl ToTokens for Route {
path, path,
guards, guards,
wrappers, wrappers,
methods,
}, },
resource_type, resource_type,
} = self; } = self;
let resource_name = name.to_string(); let resource_name = name.to_string();
let mut methods = methods.iter();
let method_guards = if *guard == GuardType::Multi {
// unwrapping since length is checked to be at least one
let first = methods.next().unwrap();
quote! {
.guard(
actix_web::guard::Any(actix_web::guard::#first())
#(.or(actix_web::guard::#methods()))*
)
}
} else {
quote! {
.guard(actix_web::guard::#guard())
}
};
let stream = quote! { let stream = quote! {
#[allow(non_camel_case_types, missing_docs)] #[allow(non_camel_case_types, missing_docs)]
pub struct #name; pub struct #name;
@ -214,7 +288,7 @@ impl ToTokens for Route {
#ast #ast
let __resource = actix_web::Resource::new(#path) let __resource = actix_web::Resource::new(#path)
.name(#resource_name) .name(#resource_name)
.guard(actix_web::guard::#guard()) #method_guards
#(.guard(actix_web::guard::fn_guard(#guards)))* #(.guard(actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))* #(.wrap(#wrappers))*
.#resource_type(#name); .#resource_type(#name);

View File

@ -5,7 +5,9 @@ use std::task::{Context, Poll};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::http::header::{HeaderName, HeaderValue}; use actix_web::http::header::{HeaderName, HeaderValue};
use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder}; use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder};
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, trace}; use actix_web_codegen::{
connect, delete, get, head, options, patch, post, put, route, trace,
};
use futures_util::future; use futures_util::future;
// Make sure that we can name function as 'config' // Make sure that we can name function as 'config'
@ -79,6 +81,11 @@ async fn get_param_test(_: Path<String>) -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
} }
#[route("/multi", method = "GET", method = "POST", method = "HEAD")]
async fn route_test() -> impl Responder {
HttpResponse::Ok()
}
pub struct ChangeStatusCode; pub struct ChangeStatusCode;
impl<S, B> Transform<S> for ChangeStatusCode impl<S, B> Transform<S> for ChangeStatusCode
@ -172,6 +179,7 @@ async fn test_body() {
.service(trace_test) .service(trace_test)
.service(patch_test) .service(patch_test)
.service(test_handler) .service(test_handler)
.service(route_test)
}); });
let request = srv.request(http::Method::GET, srv.url("/test")); let request = srv.request(http::Method::GET, srv.url("/test"));
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
@ -210,6 +218,22 @@ async fn test_body() {
let request = srv.request(http::Method::GET, srv.url("/test")); let request = srv.request(http::Method::GET, srv.url("/test"));
let response = request.send().await.unwrap(); let response = request.send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::POST, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::HEAD, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::PATCH, srv.url("/multi"));
let response = request.send().await.unwrap();
assert!(!response.status().is_success());
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -4,4 +4,24 @@ fn compile_macros() {
t.pass("tests/trybuild/simple.rs"); t.pass("tests/trybuild/simple.rs");
t.compile_fail("tests/trybuild/simple-fail.rs"); t.compile_fail("tests/trybuild/simple-fail.rs");
t.pass("tests/trybuild/route-ok.rs");
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
test_route_missing_method(&t)
} }
#[rustversion::stable(1.42)]
fn test_route_missing_method(t: &trybuild::TestCases) {
t.compile_fail("tests/trybuild/route-missing-method-fail-msrv.rs");
}
#[rustversion::not(stable(1.42))]
#[rustversion::not(nightly)]
fn test_route_missing_method(t: &trybuild::TestCases) {
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
}
#[rustversion::nightly]
fn test_route_missing_method(_t: &trybuild::TestCases) {}

View File

@ -0,0 +1,15 @@
use actix_web::*;
#[route("/", method="GET", method="GET")]
async fn index() -> impl Responder {
HttpResponse::Ok()
}
#[actix_web::main]
async fn main() {
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@ -0,0 +1,11 @@
error: HTTP Method defined more than once: `GET`
--> $DIR/route-duplicate-method-fail.rs:3:35
|
3 | #[route("/", method="GET", method="GET")]
| ^^^^^
error[E0425]: cannot find value `index` in this scope
--> $DIR/route-duplicate-method-fail.rs:10:49
|
10 | let srv = test::start(|| App::new().service(index));
| ^^^^^ not found in this scope

View File

@ -0,0 +1,15 @@
use actix_web::*;
#[route("/")]
async fn index() -> impl Responder {
HttpResponse::Ok()
}
#[actix_web::main]
async fn main() {
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@ -0,0 +1,11 @@
error: The #[route(..)] macro requires at least one `method` attribute
--> $DIR/route-missing-method-fail-msrv.rs:3:1
|
3 | #[route("/")]
| ^^^^^^^^^^^^^
error[E0425]: cannot find value `index` in this scope
--> $DIR/route-missing-method-fail-msrv.rs:10:49
|
10 | let srv = test::start(|| App::new().service(index));
| ^^^^^ not found in this scope

View File

@ -0,0 +1,15 @@
use actix_web::*;
#[route("/")]
async fn index() -> impl Responder {
HttpResponse::Ok()
}
#[actix_web::main]
async fn main() {
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@ -0,0 +1,13 @@
error: The #[route(..)] macro requires at least one `method` attribute
--> $DIR/route-missing-method-fail.rs:3:1
|
3 | #[route("/")]
| ^^^^^^^^^^^^^
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0425]: cannot find value `index` in this scope
--> $DIR/route-missing-method-fail.rs:10:49
|
10 | let srv = test::start(|| App::new().service(index));
| ^^^^^ not found in this scope

View File

@ -0,0 +1,15 @@
use actix_web::*;
#[route("/", method="GET", method="HEAD")]
async fn index() -> impl Responder {
HttpResponse::Ok()
}
#[actix_web::main]
async fn main() {
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@ -0,0 +1,15 @@
use actix_web::*;
#[route("/", method="UNEXPECTED")]
async fn index() -> impl Responder {
HttpResponse::Ok()
}
#[actix_web::main]
async fn main() {
let srv = test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@ -0,0 +1,11 @@
error: Unexpected HTTP Method: `UNEXPECTED`
--> $DIR/route-unexpected-method-fail.rs:3:21
|
3 | #[route("/", method="UNEXPECTED")]
| ^^^^^^^^^^^^
error[E0425]: cannot find value `index` in this scope
--> $DIR/route-unexpected-method-fail.rs:10:49
|
10 | let srv = test::start(|| App::new().service(index));
| ^^^^^ not found in this scope