actix_web_codegen/
scope.rs

1use 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        // 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.
66fn 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}