actix_web_codegen/
scope.rs1use proc_macro::TokenStream;
2use proc_macro2::{Span, TokenStream as TokenStream2};
3use quote::{quote, ToTokens as _};
4
5use crate::{
6 input_and_compile_error,
7 route::{MethodType, RouteArgs},
8};
9
10pub 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
17fn 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 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 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
65fn 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
99fn 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}