Skip to content

Commit

Permalink
Scope macro (#3136)
Browse files Browse the repository at this point in the history
* add scope proc macro

* Update scope macro code to work with current HttpServiceFactory

* started some test code

* add some unit tests

* code formatting cleanup

* add another test for combining and calling 2 scopes

* format code with formatter

* Update actix-web-codegen/src/lib.rs with comment documentation fix

Co-authored-by: oliver <151407407+kwfn@users.noreply.github.com>

* work in progress. revised procedural macro to change othe macro call

* add tests again. refactor nested code.

* clean up code. fix bugs with route and method attributes with parameters

* clean up for rust fmt

* clean up for rust fmt

* fix out of date comment for scope macro

* sync to master branch by adding test_wrap

* needed to format code

* test: split out scope tests

* test: add negative tests

* chore: move imports back inside (?)

* docs: tweak scope docs

* fix: prevent trailing slashes in scope prefixes

* chore: address clippy lints

---------

Co-authored-by: oliver <151407407+kwfn@users.noreply.github.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
  • Loading branch information
3 people authored Jun 7, 2024
1 parent c366649 commit 3db7891
Show file tree
Hide file tree
Showing 18 changed files with 439 additions and 17 deletions.
1 change: 1 addition & 0 deletions actix-web-codegen/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add `#[scope]` macro.
- Prevent inclusion of default `actix-router` features.
- Minimum supported Rust version (MSRV) is now 1.72.

Expand Down
50 changes: 50 additions & 0 deletions actix-web-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ use proc_macro::TokenStream;
use quote::quote;

mod route;
mod scope;

/// Creates resource handler, allowing multiple HTTP method guards.
///
Expand Down Expand Up @@ -197,6 +198,43 @@ method_macro!(Options, options);
method_macro!(Trace, trace);
method_macro!(Patch, patch);

/// Prepends a path prefix to all handlers using routing macros inside the attached module.
///
/// # Syntax
///
/// ```
/// # use actix_web_codegen::scope;
/// #[scope("/prefix")]
/// mod api {
/// // ...
/// }
/// ```
///
/// # Arguments
///
/// - `"/prefix"` - Raw literal string to be prefixed onto contained handlers' paths.
///
/// # Example
///
/// ```
/// # use actix_web_codegen::{scope, get};
/// # use actix_web::Responder;
/// #[scope("/api")]
/// mod api {
/// # use super::*;
/// #[get("/hello")]
/// pub async fn hello() -> impl Responder {
/// // this has path /api/hello
/// "Hello, world!"
/// }
/// }
/// # fn main() {}
/// ```
#[proc_macro_attribute]
pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream {
scope::with_scope(args, input)
}

/// Marks async main function as the Actix Web system entry-point.
///
/// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is
Expand Down Expand Up @@ -240,3 +278,15 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
output.extend(item);
output
}

/// 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
/// recover and show more precise errors within the macro body.
///
/// See <https://github.com/rust-analyzer/rust-analyzer/issues/10468> for more info.
fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
let compile_err = TokenStream::from(err.to_compile_error());
item.extend(compile_err);
item
}
20 changes: 5 additions & 15 deletions actix-web-codegen/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token};

use crate::input_and_compile_error;

#[derive(Debug)]
pub struct RouteArgs {
path: syn::LitStr,
options: Punctuated<syn::MetaNameValue, Token![,]>,
pub(crate) path: syn::LitStr,
pub(crate) options: Punctuated<syn::MetaNameValue, Token![,]>,
}

impl syn::parse::Parse for RouteArgs {
Expand Down Expand Up @@ -78,7 +80,7 @@ macro_rules! standard_method_type {
}
}

fn from_path(method: &Path) -> Result<Self, ()> {
pub(crate) fn from_path(method: &Path) -> Result<Self, ()> {
match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()),
Expand Down Expand Up @@ -542,15 +544,3 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
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
/// recover and show more precise errors within the macro body.
///
/// See <https://github.com/rust-analyzer/rust-analyzer/issues/10468> for more info.
fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
let compile_err = TokenStream::from(err.to_compile_error());
item.extend(compile_err);
item
}
103 changes: 103 additions & 0 deletions actix-web-codegen/src/scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens as _};

use crate::{
input_and_compile_error,
route::{MethodType, RouteArgs},
};

pub fn with_scope(args: TokenStream, input: TokenStream) -> TokenStream {
match with_scope_inner(args, input.clone()) {
Ok(stream) => stream,
Err(err) => input_and_compile_error(input, err),
}
}

fn with_scope_inner(args: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
"missing arguments for scope macro, expected: #[scope(\"/prefix\")]",
));
}

let scope_prefix = syn::parse::<syn::LitStr>(args.clone()).map_err(|err| {
syn::Error::new(
err.span(),
"argument to scope macro is not a string literal, expected: #[scope(\"/prefix\")]",
)
})?;

let scope_prefix_value = scope_prefix.value();

if scope_prefix_value.ends_with('/') {
// trailing slashes cause non-obvious problems
// it's better to point them out to developers rather than

return Err(syn::Error::new(
scope_prefix.span(),
"scopes should not have trailing slashes; see https://docs.rs/actix-web/4/actix_web/struct.Scope.html#avoid-trailing-slashes",
));
}

let mut module = syn::parse::<syn::ItemMod>(input).map_err(|err| {
syn::Error::new(err.span(), "#[scope] macro must be attached to a module")
})?;

// modify any routing macros (method or route[s]) attached to
// functions by prefixing them with this scope macro's argument
if let Some((_, items)) = &mut module.content {
for item in items {
if let syn::Item::Fn(fun) = item {
fun.attrs = fun
.attrs
.iter()
.map(|attr| modify_attribute_with_scope(attr, &scope_prefix_value))
.collect();
}
}
}

Ok(module.to_token_stream().into())
}

/// Checks if the attribute is a method type and has a route path, then modifies it.
fn modify_attribute_with_scope(attr: &syn::Attribute, scope_path: &str) -> syn::Attribute {
match (attr.parse_args::<RouteArgs>(), attr.clone().meta) {
(Ok(route_args), syn::Meta::List(meta_list)) if has_allowed_methods_in_scope(attr) => {
let modified_path = format!("{}{}", scope_path, route_args.path.value());

let options_tokens: Vec<TokenStream2> = route_args
.options
.iter()
.map(|option| {
quote! { ,#option }
})
.collect();

let combined_options_tokens: TokenStream2 =
options_tokens
.into_iter()
.fold(TokenStream2::new(), |mut acc, ts| {
acc.extend(std::iter::once(ts));
acc
});

syn::Attribute {
meta: syn::Meta::List(syn::MetaList {
tokens: quote! { #modified_path #combined_options_tokens },
..meta_list.clone()
}),
..attr.clone()
}
}
_ => attr.clone(),
}
}

fn has_allowed_methods_in_scope(attr: &syn::Attribute) -> bool {
MethodType::from_path(attr.path()).is_ok()
|| attr.path().is_ident("route")
|| attr.path().is_ident("ROUTE")
}
File renamed without changes.
Loading

0 comments on commit 3db7891

Please sign in to comment.