1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-06-26 23:17:42 +02:00

routes macro allowing multiple paths per handler (#2718)

* WIP: basic implementation for `routes` macro

* chore: changelog, docs, tests

* error on missing methods

* Apply suggestions from code review

Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>

* update test stderr expectation

* add additional tests

* fix stderr output

* remove useless ResourceType

this is dead code from back when .to and .to_async were different ways to add a service

Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>
Co-authored-by: Rob Ede <robjtede@icloud.com>
This commit is contained in:
Roland Fredenhagen
2022-07-04 06:31:49 +02:00
committed by GitHub
parent c0d5d7bdb5
commit 8759d79b03
13 changed files with 384 additions and 112 deletions

View File

@ -46,9 +46,20 @@
//! ```
//!
//! # Multiple Path Handlers
//! There are no macros to generate multi-path handlers. Let us know in [this issue].
//! Acts as a wrapper for multiple single method handler macros. It takes no arguments and
//! delegates those to the macros for the individual methods. See [macro@routes] macro docs.
//!
//! [this issue]: https://github.com/actix/actix-web/issues/1709
//! ```
//! # use actix_web::HttpResponse;
//! # use actix_web_codegen::routes;
//! #[routes]
//! #[get("/test")]
//! #[get("/test2")]
//! #[delete("/test")]
//! async fn example() -> HttpResponse {
//! HttpResponse::Ok().finish()
//! }
//! ```
//!
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
//! [GET]: macro@get
@ -104,6 +115,39 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input)
}
/// Creates resource handler, allowing multiple HTTP methods and paths.
///
/// # Syntax
/// ```plain
/// #[routes]
/// #[<method>("path", ...)]
/// #[<method>("path", ...)]
/// ...
/// ```
///
/// # Attributes
/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for
/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post).
///
/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler).
///
/// # Examples
/// ```
/// # use actix_web::HttpResponse;
/// # use actix_web_codegen::routes;
/// #[routes]
/// #[get("/test")]
/// #[get("/test2")]
/// #[delete("/test")]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
/// ```
#[proc_macro_attribute]
pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream {
route::with_methods(input)
}
macro_rules! method_macro {
($variant:ident, $method:ident) => {
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]

View File

@ -3,24 +3,12 @@ use std::{collections::HashSet, convert::TryFrom};
use actix_router::ResourceDef;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta, Path};
enum ResourceType {
Async,
Sync,
}
impl ToTokens for ResourceType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = format_ident!("to");
stream.append(ident);
}
}
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
macro_rules! method_type {
(
$($variant:ident, $upper:ident,)+
$($variant:ident, $upper:ident, $lower:ident,)+
) => {
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum MethodType {
@ -42,20 +30,27 @@ macro_rules! method_type {
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
}
}
fn from_path(method: &Path) -> Result<Self, ()> {
match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()),
}
}
}
};
}
method_type! {
Get, GET,
Post, POST,
Put, PUT,
Delete, DELETE,
Head, HEAD,
Connect, CONNECT,
Options, OPTIONS,
Trace, TRACE,
Patch, PATCH,
Get, GET, get,
Post, POST, post,
Put, PUT, put,
Delete, DELETE, delete,
Head, HEAD, head,
Connect, CONNECT, connect,
Options, OPTIONS, options,
Trace, TRACE, trace,
Patch, PATCH, patch,
}
impl ToTokens for MethodType {
@ -90,6 +85,18 @@ impl Args {
let mut wrappers = Vec::new();
let mut methods = HashSet::new();
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let is_route_macro = method.is_none();
if let Some(method) = method {
methods.insert(method);
@ -183,55 +190,27 @@ impl Args {
}
pub struct Route {
/// Name of the handler function being annotated.
name: syn::Ident,
args: Args,
/// Args passed to routing macro.
///
/// When using `#[routes]`, this will contain args for each specific routing macro.
args: Vec<Args>,
/// AST of the handler function being annotated.
ast: syn::ItemFn,
resource_type: ResourceType,
/// The doc comment attributes to copy to generated struct, if any.
doc_attributes: Vec<syn::Attribute>,
}
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
let mut guess = ResourceType::Sync;
if let syn::Type::ImplTrait(typ) = typ {
for bound in typ.bounds.iter() {
if let syn::TypeParamBound::Trait(bound) = bound {
for bound in bound.path.segments.iter() {
if bound.ident == "Future" {
guess = ResourceType::Async;
break;
} else if bound.ident == "Responder" {
guess = ResourceType::Sync;
break;
}
}
}
}
}
guess
}
impl Route {
pub fn new(
args: AttributeArgs,
ast: syn::ItemFn,
method: Option<MethodType>,
) -> syn::Result<Self> {
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<some path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct.
@ -244,6 +223,7 @@ impl Route {
.collect();
let args = Args::new(args, method)?;
if args.methods.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
@ -251,25 +231,44 @@ impl Route {
));
}
let resource_type = if ast.sig.asyncness.is_some() {
ResourceType::Async
} else {
match ast.sig.output {
syn::ReturnType::Default => {
return Err(syn::Error::new_spanned(
ast,
"Function has no return type. Cannot be used as handler",
));
}
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()),
}
};
if matches!(ast.sig.output, syn::ReturnType::Default) {
return Err(syn::Error::new_spanned(
ast,
"Function has no return type. Cannot be used as handler",
));
}
Ok(Self {
name,
args: vec![args],
ast,
doc_attributes,
})
}
fn multiple(args: Vec<Args>, ast: syn::ItemFn) -> syn::Result<Self> {
let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct.
// Note that multi line doc comments are converted to multiple doc attributes.
let doc_attributes = ast
.attrs
.iter()
.filter(|attr| attr.path.is_ident("doc"))
.cloned()
.collect();
if matches!(ast.sig.output, syn::ReturnType::Default) {
return Err(syn::Error::new_spanned(
ast,
"Function has no return type. Cannot be used as handler",
));
}
Ok(Self {
name,
args,
ast,
resource_type,
doc_attributes,
})
}
@ -280,38 +279,57 @@ impl ToTokens for Route {
let Self {
name,
ast,
args:
Args {
args,
doc_attributes,
} = self;
let registrations: TokenStream2 = args
.iter()
.map(|args| {
let Args {
path,
resource_name,
guards,
wrappers,
methods,
},
resource_type,
doc_attributes,
} = self;
let resource_name = resource_name
.as_ref()
.map_or_else(|| name.to_string(), LitStr::value);
let method_guards = {
let mut others = methods.iter();
// unwrapping since length is checked to be at least one
let first = others.next().unwrap();
} = args;
let resource_name = resource_name
.as_ref()
.map_or_else(|| name.to_string(), LitStr::value);
let method_guards = {
let mut others = methods.iter();
// unwrapping since length is checked to be at least one
let first = others.next().unwrap();
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())
}
}
};
if methods.len() > 1 {
quote! {
.guard(
::actix_web::guard::Any(::actix_web::guard::#first())
#(.or(::actix_web::guard::#others()))*
)
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);
}
} else {
quote! {
.guard(::actix_web::guard::#first())
}
}
};
})
.collect();
let stream = quote! {
#(#doc_attributes)*
@ -321,14 +339,7 @@ impl ToTokens for Route {
impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) {
#ast
let __resource = ::actix_web::Resource::new(#path)
.name(#resource_name)
#method_guards
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config)
#registrations
}
}
};
@ -357,6 +368,57 @@ pub(crate) fn with_method(
}
}
pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
let mut ast = match syn::parse::<syn::ItemFn>(input.clone()) {
Ok(ast) => ast,
// on parse error, make IDEs happy; see fn docs
Err(err) => return input_and_compile_error(input, err),
};
let (methods, others) = ast
.attrs
.into_iter()
.map(|attr| match MethodType::from_path(&attr.path) {
Ok(method) => Ok((method, attr)),
Err(_) => Err(attr),
})
.partition::<Vec<_>, _>(Result::is_ok);
ast.attrs = others.into_iter().map(Result::unwrap_err).collect();
let methods =
match methods
.into_iter()
.map(Result::unwrap)
.map(|(method, attr)| {
attr.parse_meta().and_then(|args| {
if let Meta::List(args) = args {
Args::new(args.nested.into_iter().collect(), Some(method))
} else {
Err(syn::Error::new_spanned(attr, "Invalid input for macro"))
}
})
})
.collect::<Result<Vec<_>, _>>()
{
Ok(methods) if methods.is_empty() => return input_and_compile_error(
input,
syn::Error::new(
Span::call_site(),
"The #[routes] macro requires at least one `#[<method>(..)]` attribute.",
),
),
Ok(methods) => methods,
Err(err) => return input_and_compile_error(input, err),
};
match Route::multiple(methods, ast) {
Ok(route) => route.into_token_stream().into(),
// on macro related error, make IDEs happy; see fn docs
Err(err) => input_and_compile_error(input, err),
}
}
/// Converts the error to a token stream and appends it to the original input.
///
/// Returning the original input in addition to the error is good for IDEs which can gracefully