mirror of
https://github.com/fafhrd91/actix-web
synced 2024-11-27 17:52:56 +01:00
added support for creating custom methods with route macro (#2969)
Co-authored-by: Rob Ede <robjtede@icloud.com> Closes https://github.com/actix/actix-web/issues/2893
This commit is contained in:
parent
b933ed4456
commit
65c0545a7a
@ -1,6 +1,9 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- Add support for Custom Methods with `#[route]` macro. [#2969]
|
||||||
|
|
||||||
|
[#2969]: https://github.com/actix/actix-web/pull/2969
|
||||||
|
|
||||||
|
|
||||||
## 4.1.0 - 2022-09-11
|
## 4.1.0 - 2022-09-11
|
||||||
|
@ -105,7 +105,7 @@ mod route;
|
|||||||
/// ```
|
/// ```
|
||||||
/// # use actix_web::HttpResponse;
|
/// # use actix_web::HttpResponse;
|
||||||
/// # use actix_web_codegen::route;
|
/// # use actix_web_codegen::route;
|
||||||
/// #[route("/test", method = "GET", method = "HEAD")]
|
/// #[route("/test", method = "GET", method = "HEAD", method = "CUSTOM")]
|
||||||
/// async fn example() -> HttpResponse {
|
/// async fn example() -> HttpResponse {
|
||||||
/// HttpResponse::Ok().finish()
|
/// HttpResponse::Ok().finish()
|
||||||
/// }
|
/// }
|
||||||
|
@ -27,7 +27,13 @@ macro_rules! method_type {
|
|||||||
fn parse(method: &str) -> Result<Self, String> {
|
fn parse(method: &str) -> Result<Self, String> {
|
||||||
match method {
|
match method {
|
||||||
$(stringify!($upper) => Ok(Self::$variant),)+
|
$(stringify!($upper) => Ok(Self::$variant),)+
|
||||||
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
|
_ => {
|
||||||
|
if method.chars().all(|c| c.is_ascii_uppercase()) {
|
||||||
|
Ok(Self::Method)
|
||||||
|
} else {
|
||||||
|
Err(format!("HTTP method must be uppercase: `{}`", method))
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +47,12 @@ macro_rules! method_type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, Hash, PartialEq)]
|
||||||
|
struct MethodTypeExt {
|
||||||
|
method: MethodType,
|
||||||
|
custom_method: Option<LitStr>,
|
||||||
|
}
|
||||||
|
|
||||||
method_type! {
|
method_type! {
|
||||||
Get, GET, get,
|
Get, GET, get,
|
||||||
Post, POST, post,
|
Post, POST, post,
|
||||||
@ -51,6 +63,7 @@ method_type! {
|
|||||||
Options, OPTIONS, options,
|
Options, OPTIONS, options,
|
||||||
Trace, TRACE, trace,
|
Trace, TRACE, trace,
|
||||||
Patch, PATCH, patch,
|
Patch, PATCH, patch,
|
||||||
|
Method, METHOD, method,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToTokens for MethodType {
|
impl ToTokens for MethodType {
|
||||||
@ -60,6 +73,21 @@ impl ToTokens for MethodType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ToTokens for MethodTypeExt {
|
||||||
|
fn to_tokens(&self, stream: &mut TokenStream2) {
|
||||||
|
match self.method {
|
||||||
|
MethodType::Method => {
|
||||||
|
let ident = Ident::new(
|
||||||
|
self.custom_method.as_ref().unwrap().value().as_str(),
|
||||||
|
Span::call_site(),
|
||||||
|
);
|
||||||
|
stream.append(ident);
|
||||||
|
}
|
||||||
|
_ => self.method.to_tokens(stream),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<&syn::LitStr> for MethodType {
|
impl TryFrom<&syn::LitStr> for MethodType {
|
||||||
type Error = syn::Error;
|
type Error = syn::Error;
|
||||||
|
|
||||||
@ -74,7 +102,7 @@ struct Args {
|
|||||||
resource_name: Option<syn::LitStr>,
|
resource_name: Option<syn::LitStr>,
|
||||||
guards: Vec<Path>,
|
guards: Vec<Path>,
|
||||||
wrappers: Vec<syn::Type>,
|
wrappers: Vec<syn::Type>,
|
||||||
methods: HashSet<MethodType>,
|
methods: HashSet<MethodTypeExt>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
@ -99,7 +127,12 @@ impl Args {
|
|||||||
|
|
||||||
let is_route_macro = method.is_none();
|
let is_route_macro = method.is_none();
|
||||||
if let Some(method) = method {
|
if let Some(method) = method {
|
||||||
methods.insert(method);
|
methods.insert({
|
||||||
|
MethodTypeExt {
|
||||||
|
method,
|
||||||
|
custom_method: None,
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for arg in args {
|
for arg in args {
|
||||||
@ -152,10 +185,22 @@ impl Args {
|
|||||||
));
|
));
|
||||||
} else if let syn::Lit::Str(ref lit) = nv.lit {
|
} else if let syn::Lit::Str(ref lit) = nv.lit {
|
||||||
let method = MethodType::try_from(lit)?;
|
let method = MethodType::try_from(lit)?;
|
||||||
if !methods.insert(method) {
|
if !methods.insert({
|
||||||
|
if method == MethodType::Method {
|
||||||
|
MethodTypeExt {
|
||||||
|
method,
|
||||||
|
custom_method: Some(lit.clone()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MethodTypeExt {
|
||||||
|
method,
|
||||||
|
custom_method: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
&nv.lit,
|
&nv.lit,
|
||||||
format!(
|
&format!(
|
||||||
"HTTP method defined more than once: `{}`",
|
"HTTP method defined more than once: `{}`",
|
||||||
lit.value()
|
lit.value()
|
||||||
),
|
),
|
||||||
@ -298,38 +343,72 @@ impl ToTokens for Route {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or_else(|| name.to_string(), LitStr::value);
|
.map_or_else(|| name.to_string(), LitStr::value);
|
||||||
|
|
||||||
let method_guards = {
|
let method_guards = {
|
||||||
let mut others = methods.iter();
|
let mut others = methods.iter();
|
||||||
|
let first = others.next().unwrap();
|
||||||
// unwrapping since length is checked to be at least one
|
let first_method = &first.method;
|
||||||
let first = others.next().unwrap();
|
if methods.len() > 1 {
|
||||||
|
let mut mult_method_guards: Vec<TokenStream2> = Vec::new();
|
||||||
if methods.len() > 1 {
|
for method_ext in methods {
|
||||||
quote! {
|
let method_type = &method_ext.method;
|
||||||
.guard(
|
let custom_method = &method_ext.custom_method;
|
||||||
::actix_web::guard::Any(::actix_web::guard::#first())
|
match custom_method {
|
||||||
#(.or(::actix_web::guard::#others()))*
|
Some(lit) => {
|
||||||
)
|
mult_method_guards.push(quote! {
|
||||||
}
|
.or(::actix_web::guard::#method_type(::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()))
|
||||||
} else {
|
});
|
||||||
quote! {
|
}
|
||||||
.guard(::actix_web::guard::#first())
|
None => {
|
||||||
|
mult_method_guards.push(quote! {
|
||||||
|
.or(::actix_web::guard::#method_type())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match &first.custom_method {
|
||||||
|
Some(lit) => {
|
||||||
|
quote! {
|
||||||
|
.guard(
|
||||||
|
::actix_web::guard::Any(::actix_web::guard::#first_method(::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()))
|
||||||
|
#(#mult_method_guards)*
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
quote! {
|
||||||
|
.guard(
|
||||||
|
::actix_web::guard::Any(::actix_web::guard::#first_method())
|
||||||
|
#(#mult_method_guards)*
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match &first.custom_method {
|
||||||
|
Some(lit) => {
|
||||||
|
quote! {
|
||||||
|
.guard(::actix_web::guard::#first_method(::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
quote! {
|
||||||
|
.guard(::actix_web::guard::#first_method())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
quote! {
|
||||||
|
let __resource = ::actix_web::Resource::new(#path)
|
||||||
|
.name(#resource_name)
|
||||||
|
#method_guards
|
||||||
|
#(.guard(::actix_web::guard::fn_guard(#guards)))*
|
||||||
|
#(.wrap(#wrappers))*
|
||||||
|
.to(#name);
|
||||||
|
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
|
.collect();
|
||||||
quote! {
|
|
||||||
let __resource = ::actix_web::Resource::new(#path)
|
|
||||||
.name(#resource_name)
|
|
||||||
#method_guards
|
|
||||||
#(.guard(::actix_web::guard::fn_guard(#guards)))*
|
|
||||||
#(.wrap(#wrappers))*
|
|
||||||
.to(#name);
|
|
||||||
|
|
||||||
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let stream = quote! {
|
let stream = quote! {
|
||||||
#(#doc_attributes)*
|
#(#doc_attributes)*
|
||||||
|
@ -86,7 +86,13 @@ async fn get_param_test(_: web::Path<String>) -> impl Responder {
|
|||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[route("/multi", method = "GET", method = "POST", method = "HEAD")]
|
#[route(
|
||||||
|
"/multi",
|
||||||
|
method = "GET",
|
||||||
|
method = "POST",
|
||||||
|
method = "HEAD",
|
||||||
|
method = "HELLO"
|
||||||
|
)]
|
||||||
async fn route_test() -> impl Responder {
|
async fn route_test() -> impl Responder {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,11 @@ fn compile_macros() {
|
|||||||
t.pass("tests/trybuild/route-ok.rs");
|
t.pass("tests/trybuild/route-ok.rs");
|
||||||
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
|
t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
|
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
|
|
||||||
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
|
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
|
||||||
|
|
||||||
|
t.pass("tests/trybuild/route-custom-method.rs");
|
||||||
|
t.compile_fail("tests/trybuild/route-custom-lowercase.rs");
|
||||||
|
|
||||||
t.pass("tests/trybuild/routes-ok.rs");
|
t.pass("tests/trybuild/routes-ok.rs");
|
||||||
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
|
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
|
||||||
|
19
actix-web-codegen/tests/trybuild/route-custom-lowercase.rs
Normal file
19
actix-web-codegen/tests/trybuild/route-custom-lowercase.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use actix_web_codegen::*;
|
||||||
|
use actix_web::http::Method;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
#[route("/", method = "hello")]
|
||||||
|
async fn index() -> String {
|
||||||
|
"Hello World!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
use actix_web::App;
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
|
||||||
|
let request = srv.request(Method::from_str("hello").unwrap(), srv.url("/"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
error: HTTP method must be uppercase: `hello`
|
||||||
|
--> tests/trybuild/route-custom-lowercase.rs:5:23
|
||||||
|
|
|
||||||
|
5 | #[route("/", method = "hello")]
|
||||||
|
| ^^^^^^^
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
|
||||||
|
--> tests/trybuild/route-custom-lowercase.rs:14:55
|
||||||
|
|
|
||||||
|
14 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future<Output = String> {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
||||||
|
|
|
||||||
|
note: required by a bound in `App::<T>::service`
|
||||||
|
--> $WORKSPACE/actix-web/src/app.rs
|
||||||
|
|
|
||||||
|
| F: HttpServiceFactory + 'static,
|
||||||
|
| ^^^^^^^^^^^^^^^^^^ required by this bound in `App::<T>::service`
|
@ -1,4 +1,6 @@
|
|||||||
use actix_web_codegen::*;
|
use actix_web_codegen::*;
|
||||||
|
use actix_web::http::Method;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[route("/", method="UNEXPECTED")]
|
#[route("/", method="UNEXPECTED")]
|
||||||
async fn index() -> String {
|
async fn index() -> String {
|
||||||
@ -11,7 +13,7 @@ async fn main() {
|
|||||||
|
|
||||||
let srv = actix_test::start(|| App::new().service(index));
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
|
||||||
let request = srv.get("/");
|
let request = srv.request(Method::from_str("UNEXPECTED").unwrap(), srv.url("/"));
|
||||||
let response = request.send().await.unwrap();
|
let response = request.send().await.unwrap();
|
||||||
assert!(response.status().is_success());
|
assert!(response.status().is_success());
|
||||||
}
|
}
|
@ -1,19 +0,0 @@
|
|||||||
error: Unexpected HTTP method: `UNEXPECTED`
|
|
||||||
--> tests/trybuild/route-unexpected-method-fail.rs:3:21
|
|
||||||
|
|
|
||||||
3 | #[route("/", method="UNEXPECTED")]
|
|
||||||
| ^^^^^^^^^^^^
|
|
||||||
|
|
||||||
error[E0277]: the trait bound `fn() -> impl std::future::Future<Output = String> {index}: HttpServiceFactory` is not satisfied
|
|
||||||
--> tests/trybuild/route-unexpected-method-fail.rs:12:55
|
|
||||||
|
|
|
||||||
12 | let srv = actix_test::start(|| App::new().service(index));
|
|
||||||
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future<Output = String> {index}`
|
|
||||||
| |
|
|
||||||
| required by a bound introduced by this call
|
|
||||||
|
|
|
||||||
note: required by a bound in `App::<T>::service`
|
|
||||||
--> $WORKSPACE/actix-web/src/app.rs
|
|
||||||
|
|
|
||||||
| F: HttpServiceFactory + 'static,
|
|
||||||
| ^^^^^^^^^^^^^^^^^^ required by this bound in `App::<T>::service`
|
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
|
||||||
## 4.3.0 - 2023-01-21
|
## 4.3.0 - 2023-01-21
|
||||||
### Added
|
### Added
|
||||||
- Add `ContentDisposition::attachment()` constructor. [#2867]
|
- Add `ContentDisposition::attachment()` constructor. [#2867]
|
||||||
|
Loading…
Reference in New Issue
Block a user