1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-27 17:52:56 +01:00

Unify route macros (#1705)

This commit is contained in:
Arniu Tseng 2020-09-23 05:42:51 +08:00 committed by GitHub
parent f7bcad9567
commit 162121bf8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 182 additions and 222 deletions

View File

@ -1,161 +1,103 @@
#![recursion_limit = "512"] #![recursion_limit = "512"]
//! Helper and convenience macros for Actix-web.
//!
//! ## Runtime Setup
//!
//! - [main](attr.main.html)
//!
//! ## Resource Macros:
//!
//! - [get](attr.get.html)
//! - [post](attr.post.html)
//! - [put](attr.put.html)
//! - [delete](attr.delete.html)
//! - [head](attr.head.html)
//! - [connect](attr.connect.html)
//! - [options](attr.options.html)
//! - [trace](attr.trace.html)
//! - [patch](attr.patch.html)
//!
//! ### Attributes:
//!
//! - `"path"` - Raw literal string with path for which to register handle. Mandatory.
//! - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
//! - `wrap="Middleware"` - Registers a resource middleware.
//!
//! ### Notes
//!
//! Function name can be specified as any expression that is going to be accessible to the generate
//! code (e.g `my_guard` or `my_module::my_guard`)
//!
//! ### Example:
//!
//! ```rust
//! use actix_web::HttpResponse;
//! use actix_web_codegen::get;
//!
//! #[get("/test")]
//! async fn async_test() -> Result<HttpResponse, actix_web::Error> {
//! Ok(HttpResponse::Ok().finish())
//! }
//! ```
extern crate proc_macro; extern crate proc_macro;
mod route;
use proc_macro::TokenStream; use proc_macro::TokenStream;
/// Creates route handler with `GET` method guard. mod route;
///
/// Syntax: `#[get("path" [, attributes])]`
///
/// ## Attributes:
///
/// - `"path"` - Raw literal string with path for which to register handler. Mandatory.
/// - `guard = "function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
/// - `wrap = "Middleware"` - Registers a resource middleware.
#[proc_macro_attribute]
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Get)
}
/// Creates route handler with `POST` method guard.
///
/// Syntax: `#[post("path" [, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html)
#[proc_macro_attribute]
pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Post)
}
/// Creates route handler with `PUT` method guard.
///
/// Syntax: `#[put("path" [, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html)
#[proc_macro_attribute]
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Put)
}
/// Creates route handler with `DELETE` method guard.
///
/// Syntax: `#[delete("path" [, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html).
#[proc_macro_attribute]
pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Delete)
}
/// Creates route handler with `HEAD` method guard.
///
/// Syntax: `#[head("path" [, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html).
#[proc_macro_attribute]
pub fn head(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Head)
}
/// Creates route handler with `CONNECT` method guard.
///
/// Syntax: `#[connect("path" [, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html).
#[proc_macro_attribute]
pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Connect)
}
/// Creates route handler with `OPTIONS` method guard.
///
/// Syntax: `#[options("path" [, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html).
#[proc_macro_attribute]
pub fn options(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Options)
}
/// Creates route handler with `TRACE` method guard.
///
/// Syntax: `#[trace("path" [, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html).
#[proc_macro_attribute]
pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Trace)
}
/// Creates route handler with `PATCH` method guard.
///
/// Syntax: `#[patch("path" [, attributes])]`
///
/// Attributes are the same as in [get](attr.get.html).
#[proc_macro_attribute]
pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Patch)
}
/// Creates resource handler, allowing multiple HTTP method guards. /// Creates resource handler, allowing multiple HTTP method guards.
/// ///
/// Syntax: `#[route("path"[, attributes])]` /// ## Syntax
/// ```text
/// #[route("path", method="HTTP_METHOD"[, attributes])]
/// ```
/// ///
/// Example: `#[route("/", method="GET", method="HEAD")]` /// ### Attributes
/// /// - `"path"` - Raw literal string with path for which to register handler.
/// ## Attributes /// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example.
///
/// - `"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` /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`
/// - `wrap="Middleware"` - Registers a resource middleware. /// - `wrap="Middleware"` - Registers a resource middleware.
///
/// ### Notes
/// Function name can be specified as any expression that is going to be accessible to the generate
/// code, e.g `my_guard` or `my_module::my_guard`.
///
/// ## Example
///
/// ```rust
/// use actix_web::HttpResponse;
/// use actix_web_codegen::route;
///
/// #[route("/", method="GET", method="HEAD")]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
/// ```
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn route(args: TokenStream, input: TokenStream) -> TokenStream { pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::generate(args, input, route::GuardType::Multi) route::with_method(None, args, input)
}
macro_rules! doc_comment {
($x:expr; $($tt:tt)*) => {
#[doc = $x]
$($tt)*
};
}
macro_rules! method_macro {
(
$($variant:ident, $method:ident,)+
) => {
$(doc_comment! {
concat!("
Creates route handler with `actix_web::guard::", stringify!($variant), "`.
## Syntax
```text
#[", stringify!($method), r#"("path"[, attributes])]
```
### Attributes
- `"path"` - Raw literal string with path for which to register handler.
- `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`.
- `wrap="Middleware"` - Registers a resource middleware.
### Notes
Function name can be specified as any expression that is going to be accessible to the generate
code, e.g `my_guard` or `my_module::my_guard`.
## Example
```rust
use actix_web::HttpResponse;
use actix_web_codegen::"#, stringify!($method), ";
#[", stringify!($method), r#"("/")]
async fn example() -> HttpResponse {
HttpResponse::Ok().finish()
}
```
"#);
#[proc_macro_attribute]
pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(Some(route::MethodType::$variant), args, input)
}
})+
};
}
method_macro! {
Get, get,
Post, post,
Put, put,
Delete, delete,
Head, head,
Connect, connect,
Options, options,
Trace, trace,
Patch, patch,
} }
/// Marks async main function as the actix system entry-point. /// Marks async main function as the actix system entry-point.

View File

@ -20,63 +20,59 @@ impl ToTokens for ResourceType {
} }
} }
#[derive(Debug, PartialEq, Eq, Hash)] macro_rules! method_type {
pub enum GuardType { (
Get, $($variant:ident, $upper:ident,)+
Post, ) => {
Put, #[derive(Debug, PartialEq, Eq, Hash)]
Delete, pub enum MethodType {
Head, $(
Connect, $variant,
Options, )+
Trace, }
Patch,
Multi,
}
impl GuardType { impl MethodType {
fn as_str(&self) -> &'static str { fn as_str(&self) -> &'static str {
match self { match self {
GuardType::Get => "Get", $(Self::$variant => stringify!($variant),)+
GuardType::Post => "Post",
GuardType::Put => "Put",
GuardType::Delete => "Delete",
GuardType::Head => "Head",
GuardType::Connect => "Connect",
GuardType::Options => "Options",
GuardType::Trace => "Trace",
GuardType::Patch => "Patch",
GuardType::Multi => "Multi",
} }
} }
fn parse(method: &str) -> Result<Self, String> {
match method {
$(stringify!($upper) => Ok(Self::$variant),)+
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
}
}
}
};
} }
impl ToTokens for GuardType { method_type! {
Get, GET,
Post, POST,
Put, PUT,
Delete, DELETE,
Head, HEAD,
Connect, CONNECT,
Options, OPTIONS,
Trace, TRACE,
Patch, PATCH,
}
impl ToTokens for MethodType {
fn to_tokens(&self, stream: &mut TokenStream2) { fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = Ident::new(self.as_str(), Span::call_site()); let ident = Ident::new(self.as_str(), Span::call_site());
stream.append(ident); stream.append(ident);
} }
} }
impl TryFrom<&syn::LitStr> for GuardType { impl TryFrom<&syn::LitStr> for MethodType {
type Error = syn::Error; type Error = syn::Error;
fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> { fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
match value.value().as_str() { Self::parse(value.value().as_str())
"CONNECT" => Ok(GuardType::Connect), .map_err(|message| syn::Error::new_spanned(value, message))
"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()),
)),
}
} }
} }
@ -84,15 +80,21 @@ 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>, methods: HashSet<MethodType>,
} }
impl Args { impl Args {
fn new(args: AttributeArgs) -> syn::Result<Self> { fn new(args: AttributeArgs, method: Option<MethodType>) -> syn::Result<Self> {
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(); let mut methods = HashSet::new();
let is_route_macro = method.is_none();
if let Some(method) = method {
methods.insert(method);
}
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 {
@ -126,13 +128,18 @@ impl Args {
)); ));
} }
} else if nv.path.is_ident("method") { } else if nv.path.is_ident("method") {
if let syn::Lit::Str(ref lit) = nv.lit { if !is_route_macro {
let guard = GuardType::try_from(lit)?; return Err(syn::Error::new_spanned(
if !methods.insert(guard) { &nv,
"HTTP method forbidden here. To handle multiple methods, use `route` instead",
));
} else if let syn::Lit::Str(ref lit) = nv.lit {
let method = MethodType::try_from(lit)?;
if !methods.insert(method) {
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()
), ),
)); ));
@ -169,7 +176,6 @@ pub struct Route {
args: Args, args: Args,
ast: syn::ItemFn, ast: syn::ItemFn,
resource_type: ResourceType, resource_type: ResourceType,
guard: GuardType,
} }
fn guess_resource_type(typ: &syn::Type) -> ResourceType { fn guess_resource_type(typ: &syn::Type) -> ResourceType {
@ -198,23 +204,25 @@ impl Route {
pub fn new( pub fn new(
args: AttributeArgs, args: AttributeArgs,
input: TokenStream, input: TokenStream,
guard: GuardType, method: Option<MethodType>,
) -> syn::Result<Self> { ) -> syn::Result<Self> {
if args.is_empty() { if args.is_empty() {
return Err(syn::Error::new( return Err(syn::Error::new(
Span::call_site(), Span::call_site(),
format!( format!(
r#"invalid server definition, expected #[{}("<some path>")]"#, r#"invalid service definition, expected #[{}("<some path>")]"#,
guard.as_str().to_ascii_lowercase() method
.map(|it| it.as_str())
.unwrap_or("route")
.to_ascii_lowercase()
), ),
)); ));
} }
let ast: syn::ItemFn = syn::parse(input)?; let ast: syn::ItemFn = syn::parse(input)?;
let name = ast.sig.ident.clone(); let name = ast.sig.ident.clone();
let args = Args::new(args)?; let args = Args::new(args, method)?;
if args.methods.is_empty() {
if guard == GuardType::Multi && args.methods.is_empty() {
return Err(syn::Error::new( return Err(syn::Error::new(
Span::call_site(), Span::call_site(),
"The #[route(..)] macro requires at least one `method` attribute", "The #[route(..)] macro requires at least one `method` attribute",
@ -240,7 +248,6 @@ impl Route {
args, args,
ast, ast,
resource_type, resource_type,
guard,
}) })
} }
} }
@ -249,7 +256,6 @@ impl ToTokens for Route {
fn to_tokens(&self, output: &mut TokenStream2) { fn to_tokens(&self, output: &mut TokenStream2) {
let Self { let Self {
name, name,
guard,
ast, ast,
args: args:
Args { Args {
@ -261,21 +267,22 @@ impl ToTokens for Route {
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 = {
let mut others = methods.iter();
let method_guards = if *guard == GuardType::Multi {
// unwrapping since length is checked to be at least one // unwrapping since length is checked to be at least one
let first = methods.next().unwrap(); let first = others.next().unwrap();
if methods.len() > 1 {
quote! { quote! {
.guard( .guard(
actix_web::guard::Any(actix_web::guard::#first()) actix_web::guard::Any(actix_web::guard::#first())
#(.or(actix_web::guard::#methods()))* #(.or(actix_web::guard::#others()))*
) )
} }
} else { } else {
quote! { quote! {
.guard(actix_web::guard::#guard()) .guard(actix_web::guard::#first())
}
} }
}; };
@ -302,13 +309,13 @@ impl ToTokens for Route {
} }
} }
pub(crate) fn generate( pub(crate) fn with_method(
method: Option<MethodType>,
args: TokenStream, args: TokenStream,
input: TokenStream, input: TokenStream,
guard: GuardType,
) -> TokenStream { ) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
match Route::new(args, input, guard) { match Route::new(args, input, method) {
Ok(route) => route.into_token_stream().into(), Ok(route) => route.into_token_stream().into(),
Err(err) => err.to_compile_error().into(), Err(err) => err.to_compile_error().into(),
} }

View File

@ -1,4 +1,4 @@
error: HTTP Method defined more than once: `GET` error: HTTP method defined more than once: `GET`
--> $DIR/route-duplicate-method-fail.rs:3:35 --> $DIR/route-duplicate-method-fail.rs:3:35
| |
3 | #[route("/", method="GET", method="GET")] 3 | #[route("/", method="GET", method="GET")]

View File

@ -1,4 +1,4 @@
error: Unexpected HTTP Method: `UNEXPECTED` error: Unexpected HTTP method: `UNEXPECTED`
--> $DIR/route-unexpected-method-fail.rs:3:21 --> $DIR/route-unexpected-method-fail.rs:3:21
| |
3 | #[route("/", method="UNEXPECTED")] 3 | #[route("/", method="UNEXPECTED")]

View File

@ -22,4 +22,9 @@ async fn four() -> impl Responder {
HttpResponse::Ok() HttpResponse::Ok()
} }
#[delete("/five", method="GET")]
async fn five() -> impl Responder {
HttpResponse::Ok()
}
fn main() {} fn main() {}

View File

@ -21,3 +21,9 @@ error: Multiple paths specified! Should be only one!
| |
20 | #[delete("/four", "/five")] 20 | #[delete("/four", "/five")]
| ^^^^^^^ | ^^^^^^^
error: HTTP method forbidden here. To handle multiple methods, use `route` instead
--> $DIR/simple-fail.rs:25:19
|
25 | #[delete("/five", method="GET")]
| ^^^^^^^^^^^^