Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

routes macro allowing multiple paths per handler #2718

Merged
merged 11 commits into from Jul 4, 2022
5 changes: 4 additions & 1 deletion actix-web-codegen/CHANGES.md
@@ -1,6 +1,9 @@
# Changes

## Unreleased - 2021-xx-xx
## Unreleased - 2022-xx-xx
- Added `#[actix_web::routes]` macro to support multiple routes on one handler

[#2718]: https://github.com/actix/actix-web/pull/2718


## 4.0.0 - 2022-02-24
Expand Down
48 changes: 46 additions & 2 deletions actix-web-codegen/src/lib.rs
Expand Up @@ -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
Expand Down Expand Up @@ -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 it self has no parameters, but allows specifying the attribute macros for
ModProg marked this conversation as resolved.
Show resolved Hide resolved
/// the different methods, e.g. [`GET`](macro@get) or [`POST`](macro@post).
ModProg marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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), "`.")]
Expand Down
217 changes: 156 additions & 61 deletions actix-web-codegen/src/route.rs
Expand Up @@ -4,7 +4,7 @@ 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};
use syn::{parse_macro_input, Attribute, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};

enum ResourceType {
Async,
Expand All @@ -20,7 +20,7 @@ impl ToTokens for ResourceType {

macro_rules! method_type {
(
$($variant:ident, $upper:ident,)+
$($variant:ident, $upper:ident, $lower:ident,)+
) => {
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum MethodType {
Expand All @@ -42,20 +42,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 {
Expand Down Expand Up @@ -90,6 +97,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 #[{}("<some 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);
Expand Down Expand Up @@ -184,7 +203,7 @@ impl Args {

pub struct Route {
name: syn::Ident,
args: Args,
args: Vec<Args>,
ast: syn::ItemFn,
resource_type: ResourceType,

Expand Down Expand Up @@ -220,18 +239,6 @@ impl Route {
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.
Expand Down Expand Up @@ -265,6 +272,41 @@ impl Route {
}
};

Ok(Self {
name,
args: vec![args],
ast,
resource_type,
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();

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

Ok(Self {
name,
args,
Expand All @@ -280,38 +322,55 @@ impl ToTokens for Route {
let Self {
name,
ast,
args:
Args {
path,
resource_name,
guards,
wrappers,
methods,
},
args,
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();

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

let registrations: TokenStream2 = args
.iter()
.map(
|Args {
path,
resource_name,
guards,
wrappers,
methods,
}| {
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())
}
}
};
quote! {
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);
}
},
)
.collect();

let stream = quote! {
#(#doc_attributes)*
Expand All @@ -321,14 +380,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
}
}
};
Expand Down Expand Up @@ -357,6 +409,49 @@ 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): (Vec<Result<(MethodType, Attribute), Attribute>>, _) = ast
.attrs
.into_iter()
.map(|attr| match MethodType::from_path(&attr.path) {
Ok(method) => Ok((method, attr)),
Err(_) => Err(attr),
})
.partition(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()
{
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
Expand Down