Skip to content

Commit 3db7891

Browse files
pmd3doliverrobjtede
authored
Scope macro (#3136)
* 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 <[email protected]> * 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 <[email protected]> Co-authored-by: Rob Ede <[email protected]>
1 parent c366649 commit 3db7891

18 files changed

+439
-17
lines changed

actix-web-codegen/CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

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

actix-web-codegen/src/lib.rs

+50
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ use proc_macro::TokenStream;
8383
use quote::quote;
8484

8585
mod route;
86+
mod scope;
8687

8788
/// Creates resource handler, allowing multiple HTTP method guards.
8889
///
@@ -197,6 +198,43 @@ method_macro!(Options, options);
197198
method_macro!(Trace, trace);
198199
method_macro!(Patch, patch);
199200

201+
/// Prepends a path prefix to all handlers using routing macros inside the attached module.
202+
///
203+
/// # Syntax
204+
///
205+
/// ```
206+
/// # use actix_web_codegen::scope;
207+
/// #[scope("/prefix")]
208+
/// mod api {
209+
/// // ...
210+
/// }
211+
/// ```
212+
///
213+
/// # Arguments
214+
///
215+
/// - `"/prefix"` - Raw literal string to be prefixed onto contained handlers' paths.
216+
///
217+
/// # Example
218+
///
219+
/// ```
220+
/// # use actix_web_codegen::{scope, get};
221+
/// # use actix_web::Responder;
222+
/// #[scope("/api")]
223+
/// mod api {
224+
/// # use super::*;
225+
/// #[get("/hello")]
226+
/// pub async fn hello() -> impl Responder {
227+
/// // this has path /api/hello
228+
/// "Hello, world!"
229+
/// }
230+
/// }
231+
/// # fn main() {}
232+
/// ```
233+
#[proc_macro_attribute]
234+
pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream {
235+
scope::with_scope(args, input)
236+
}
237+
200238
/// Marks async main function as the Actix Web system entry-point.
201239
///
202240
/// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is
@@ -240,3 +278,15 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream {
240278
output.extend(item);
241279
output
242280
}
281+
282+
/// Converts the error to a token stream and appends it to the original input.
283+
///
284+
/// Returning the original input in addition to the error is good for IDEs which can gracefully
285+
/// recover and show more precise errors within the macro body.
286+
///
287+
/// See <https://github.com/rust-analyzer/rust-analyzer/issues/10468> for more info.
288+
fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
289+
let compile_err = TokenStream::from(err.to_compile_error());
290+
item.extend(compile_err);
291+
item
292+
}

actix-web-codegen/src/route.rs

+5-15
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ use proc_macro2::{Span, TokenStream as TokenStream2};
66
use quote::{quote, ToTokens, TokenStreamExt};
77
use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token};
88

9+
use crate::input_and_compile_error;
10+
911
#[derive(Debug)]
1012
pub struct RouteArgs {
11-
path: syn::LitStr,
12-
options: Punctuated<syn::MetaNameValue, Token![,]>,
13+
pub(crate) path: syn::LitStr,
14+
pub(crate) options: Punctuated<syn::MetaNameValue, Token![,]>,
1315
}
1416

1517
impl syn::parse::Parse for RouteArgs {
@@ -78,7 +80,7 @@ macro_rules! standard_method_type {
7880
}
7981
}
8082

81-
fn from_path(method: &Path) -> Result<Self, ()> {
83+
pub(crate) fn from_path(method: &Path) -> Result<Self, ()> {
8284
match () {
8385
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
8486
_ => Err(()),
@@ -542,15 +544,3 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
542544
Err(err) => input_and_compile_error(input, err),
543545
}
544546
}
545-
546-
/// Converts the error to a token stream and appends it to the original input.
547-
///
548-
/// Returning the original input in addition to the error is good for IDEs which can gracefully
549-
/// recover and show more precise errors within the macro body.
550-
///
551-
/// See <https://github.com/rust-analyzer/rust-analyzer/issues/10468> for more info.
552-
fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream {
553-
let compile_err = TokenStream::from(err.to_compile_error());
554-
item.extend(compile_err);
555-
item
556-
}

actix-web-codegen/src/scope.rs

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use proc_macro::TokenStream;
2+
use proc_macro2::{Span, TokenStream as TokenStream2};
3+
use quote::{quote, ToTokens as _};
4+
5+
use crate::{
6+
input_and_compile_error,
7+
route::{MethodType, RouteArgs},
8+
};
9+
10+
pub fn with_scope(args: TokenStream, input: TokenStream) -> TokenStream {
11+
match with_scope_inner(args, input.clone()) {
12+
Ok(stream) => stream,
13+
Err(err) => input_and_compile_error(input, err),
14+
}
15+
}
16+
17+
fn with_scope_inner(args: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
18+
if args.is_empty() {
19+
return Err(syn::Error::new(
20+
Span::call_site(),
21+
"missing arguments for scope macro, expected: #[scope(\"/prefix\")]",
22+
));
23+
}
24+
25+
let scope_prefix = syn::parse::<syn::LitStr>(args.clone()).map_err(|err| {
26+
syn::Error::new(
27+
err.span(),
28+
"argument to scope macro is not a string literal, expected: #[scope(\"/prefix\")]",
29+
)
30+
})?;
31+
32+
let scope_prefix_value = scope_prefix.value();
33+
34+
if scope_prefix_value.ends_with('/') {
35+
// trailing slashes cause non-obvious problems
36+
// it's better to point them out to developers rather than
37+
38+
return Err(syn::Error::new(
39+
scope_prefix.span(),
40+
"scopes should not have trailing slashes; see https://docs.rs/actix-web/4/actix_web/struct.Scope.html#avoid-trailing-slashes",
41+
));
42+
}
43+
44+
let mut module = syn::parse::<syn::ItemMod>(input).map_err(|err| {
45+
syn::Error::new(err.span(), "#[scope] macro must be attached to a module")
46+
})?;
47+
48+
// modify any routing macros (method or route[s]) attached to
49+
// functions by prefixing them with this scope macro's argument
50+
if let Some((_, items)) = &mut module.content {
51+
for item in items {
52+
if let syn::Item::Fn(fun) = item {
53+
fun.attrs = fun
54+
.attrs
55+
.iter()
56+
.map(|attr| modify_attribute_with_scope(attr, &scope_prefix_value))
57+
.collect();
58+
}
59+
}
60+
}
61+
62+
Ok(module.to_token_stream().into())
63+
}
64+
65+
/// Checks if the attribute is a method type and has a route path, then modifies it.
66+
fn modify_attribute_with_scope(attr: &syn::Attribute, scope_path: &str) -> syn::Attribute {
67+
match (attr.parse_args::<RouteArgs>(), attr.clone().meta) {
68+
(Ok(route_args), syn::Meta::List(meta_list)) if has_allowed_methods_in_scope(attr) => {
69+
let modified_path = format!("{}{}", scope_path, route_args.path.value());
70+
71+
let options_tokens: Vec<TokenStream2> = route_args
72+
.options
73+
.iter()
74+
.map(|option| {
75+
quote! { ,#option }
76+
})
77+
.collect();
78+
79+
let combined_options_tokens: TokenStream2 =
80+
options_tokens
81+
.into_iter()
82+
.fold(TokenStream2::new(), |mut acc, ts| {
83+
acc.extend(std::iter::once(ts));
84+
acc
85+
});
86+
87+
syn::Attribute {
88+
meta: syn::Meta::List(syn::MetaList {
89+
tokens: quote! { #modified_path #combined_options_tokens },
90+
..meta_list.clone()
91+
}),
92+
..attr.clone()
93+
}
94+
}
95+
_ => attr.clone(),
96+
}
97+
}
98+
99+
fn has_allowed_methods_in_scope(attr: &syn::Attribute) -> bool {
100+
MethodType::from_path(attr.path()).is_ok()
101+
|| attr.path().is_ident("route")
102+
|| attr.path().is_ident("ROUTE")
103+
}

0 commit comments

Comments
 (0)