1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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")
}