1
0
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:
edgerunnergit 2023-02-06 18:10:41 +05:30 committed by GitHub
parent b933ed4456
commit 65c0545a7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 169 additions and 59 deletions

View File

@ -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

View File

@ -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()
/// } /// }

View File

@ -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)*

View File

@ -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()
} }

View File

@ -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");

View 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());
}

View File

@ -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`

View File

@ -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());
} }

View File

@ -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`

View File

@ -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]