diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 7ae6a26b1..62a1cc5fa 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,161 +1,103 @@ #![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 { -//! Ok(HttpResponse::Ok().finish()) -//! } -//! ``` - extern crate proc_macro; -mod route; - use proc_macro::TokenStream; -/// Creates route handler with `GET` method guard. -/// -/// 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) -} +mod route; /// 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. Mandatory. -/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. +/// ### Attributes +/// - `"path"` - Raw literal string with path for which to register handler. +/// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// - `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::route; +/// +/// #[route("/", method="GET", method="HEAD")] +/// async fn example() -> HttpResponse { +/// HttpResponse::Ok().finish() +/// } +/// ``` #[proc_macro_attribute] 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. diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 394ced212..ddbd42454 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -20,63 +20,59 @@ impl ToTokens for ResourceType { } } -#[derive(Debug, PartialEq, Eq, Hash)] -pub enum GuardType { - Get, - Post, - Put, - Delete, - Head, - Connect, - Options, - Trace, - Patch, - Multi, -} - -impl GuardType { - fn as_str(&self) -> &'static str { - match self { - GuardType::Get => "Get", - 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", +macro_rules! method_type { + ( + $($variant:ident, $upper:ident,)+ + ) => { + #[derive(Debug, PartialEq, Eq, Hash)] + pub enum MethodType { + $( + $variant, + )+ } - } + + impl MethodType { + fn as_str(&self) -> &'static str { + match self { + $(Self::$variant => stringify!($variant),)+ + } + } + + fn parse(method: &str) -> Result { + 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) { let ident = Ident::new(self.as_str(), Span::call_site()); stream.append(ident); } } -impl TryFrom<&syn::LitStr> for GuardType { +impl TryFrom<&syn::LitStr> for MethodType { type Error = syn::Error; fn try_from(value: &syn::LitStr) -> Result { - 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()), - )), - } + Self::parse(value.value().as_str()) + .map_err(|message| syn::Error::new_spanned(value, message)) } } @@ -84,15 +80,21 @@ struct Args { path: syn::LitStr, guards: Vec, wrappers: Vec, - methods: HashSet, + methods: HashSet, } impl Args { - fn new(args: AttributeArgs) -> syn::Result { + fn new(args: AttributeArgs, method: Option) -> syn::Result { let mut path = None; let mut guards = Vec::new(); let mut wrappers = Vec::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 { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { @@ -126,13 +128,18 @@ impl Args { )); } } 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) { + if !is_route_macro { + return Err(syn::Error::new_spanned( + &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( &nv.lit, &format!( - "HTTP Method defined more than once: `{}`", + "HTTP method defined more than once: `{}`", lit.value() ), )); @@ -169,7 +176,6 @@ pub struct Route { args: Args, ast: syn::ItemFn, resource_type: ResourceType, - guard: GuardType, } fn guess_resource_type(typ: &syn::Type) -> ResourceType { @@ -198,23 +204,25 @@ impl Route { pub fn new( args: AttributeArgs, input: TokenStream, - guard: GuardType, + method: Option, ) -> syn::Result { if args.is_empty() { return Err(syn::Error::new( Span::call_site(), format!( - r#"invalid server definition, expected #[{}("")]"#, - guard.as_str().to_ascii_lowercase() + r#"invalid service definition, expected #[{}("")]"#, + method + .map(|it| it.as_str()) + .unwrap_or("route") + .to_ascii_lowercase() ), )); } let ast: syn::ItemFn = syn::parse(input)?; let name = ast.sig.ident.clone(); - let args = Args::new(args)?; - - if guard == GuardType::Multi && args.methods.is_empty() { + let args = Args::new(args, method)?; + if args.methods.is_empty() { return Err(syn::Error::new( Span::call_site(), "The #[route(..)] macro requires at least one `method` attribute", @@ -240,7 +248,6 @@ impl Route { args, ast, resource_type, - guard, }) } } @@ -249,7 +256,6 @@ impl ToTokens for Route { fn to_tokens(&self, output: &mut TokenStream2) { let Self { name, - guard, ast, args: Args { @@ -261,21 +267,22 @@ impl ToTokens for Route { resource_type, } = self; let resource_name = name.to_string(); - let mut methods = methods.iter(); - - let method_guards = if *guard == GuardType::Multi { + let method_guards = { + let mut others = methods.iter(); // unwrapping since length is checked to be at least one - let first = methods.next().unwrap(); + let first = others.next().unwrap(); - quote! { - .guard( - actix_web::guard::Any(actix_web::guard::#first()) - #(.or(actix_web::guard::#methods()))* - ) - } - } else { - quote! { - .guard(actix_web::guard::#guard()) + if methods.len() > 1 { + quote! { + .guard( + actix_web::guard::Any(actix_web::guard::#first()) + #(.or(actix_web::guard::#others()))* + ) + } + } else { + quote! { + .guard(actix_web::guard::#first()) + } } }; @@ -302,13 +309,13 @@ impl ToTokens for Route { } } -pub(crate) fn generate( +pub(crate) fn with_method( + method: Option, args: TokenStream, input: TokenStream, - guard: GuardType, ) -> TokenStream { 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(), Err(err) => err.to_compile_error().into(), } diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr index 8bf857c4d..613054de5 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -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 | 3 | #[route("/", method="GET", method="GET")] diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr index 3fe49f774..fe17fdf12 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -1,4 +1,4 @@ -error: Unexpected HTTP Method: `UNEXPECTED` +error: Unexpected HTTP method: `UNEXPECTED` --> $DIR/route-unexpected-method-fail.rs:3:21 | 3 | #[route("/", method="UNEXPECTED")] diff --git a/actix-web-codegen/tests/trybuild/simple-fail.rs b/actix-web-codegen/tests/trybuild/simple-fail.rs index 140497687..368cff046 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.rs +++ b/actix-web-codegen/tests/trybuild/simple-fail.rs @@ -22,4 +22,9 @@ async fn four() -> impl Responder { HttpResponse::Ok() } +#[delete("/five", method="GET")] +async fn five() -> impl Responder { + HttpResponse::Ok() +} + fn main() {} diff --git a/actix-web-codegen/tests/trybuild/simple-fail.stderr b/actix-web-codegen/tests/trybuild/simple-fail.stderr index 12c32c00d..cffc81ff8 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.stderr +++ b/actix-web-codegen/tests/trybuild/simple-fail.stderr @@ -21,3 +21,9 @@ error: Multiple paths specified! Should be only one! | 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")] + | ^^^^^^^^^^^^