actix_web_codegen/
route.rs

1use std::collections::HashSet;
2
3use actix_router::ResourceDef;
4use proc_macro::TokenStream;
5use proc_macro2::{Span, TokenStream as TokenStream2};
6use quote::{quote, ToTokens, TokenStreamExt};
7use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token};
8
9use crate::input_and_compile_error;
10
11#[derive(Debug)]
12pub struct RouteArgs {
13    pub(crate) path: syn::LitStr,
14    pub(crate) options: Punctuated<syn::MetaNameValue, Token![,]>,
15}
16
17impl syn::parse::Parse for RouteArgs {
18    fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
19        // path to match: "/foo"
20        let path = input.parse::<syn::LitStr>().map_err(|mut err| {
21            err.combine(syn::Error::new(
22                err.span(),
23                r#"invalid service definition, expected #[<method>("<path>")]"#,
24            ));
25
26            err
27        })?;
28
29        // verify that path pattern is valid
30        let _ = ResourceDef::new(path.value());
31
32        // if there's no comma, assume that no options are provided
33        if !input.peek(Token![,]) {
34            return Ok(Self {
35                path,
36                options: Punctuated::new(),
37            });
38        }
39
40        // advance past comma separator
41        input.parse::<Token![,]>()?;
42
43        // if next char is a literal, assume that it is a string and show multi-path error
44        if input.cursor().literal().is_some() {
45            return Err(syn::Error::new(
46                Span::call_site(),
47                r#"Multiple paths specified! There should be only one."#,
48            ));
49        }
50
51        // zero or more options: name = "foo"
52        let options = input.parse_terminated(syn::MetaNameValue::parse, Token![,])?;
53
54        Ok(Self { path, options })
55    }
56}
57
58macro_rules! standard_method_type {
59    (
60        $($variant:ident, $upper:ident, $lower:ident,)+
61    ) => {
62        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
63        pub enum MethodType {
64            $(
65                $variant,
66            )+
67        }
68
69        impl MethodType {
70            fn as_str(&self) -> &'static str {
71                match self {
72                    $(Self::$variant => stringify!($variant),)+
73                }
74            }
75
76            fn parse(method: &str) -> Result<Self, String> {
77                match method {
78                    $(stringify!($upper) => Ok(Self::$variant),)+
79                    _ => Err(format!("HTTP method must be uppercase: `{}`", method)),
80                }
81            }
82
83            pub(crate) fn from_path(method: &Path) -> Result<Self, ()> {
84                match () {
85                    $(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
86                    _ => Err(()),
87                }
88            }
89        }
90    };
91}
92
93standard_method_type! {
94    Get,       GET,     get,
95    Post,      POST,    post,
96    Put,       PUT,     put,
97    Delete,    DELETE,  delete,
98    Head,      HEAD,    head,
99    Connect,   CONNECT, connect,
100    Options,   OPTIONS, options,
101    Trace,     TRACE,   trace,
102    Patch,     PATCH,   patch,
103}
104
105impl TryFrom<&syn::LitStr> for MethodType {
106    type Error = syn::Error;
107
108    fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
109        Self::parse(value.value().as_str())
110            .map_err(|message| syn::Error::new_spanned(value, message))
111    }
112}
113
114impl ToTokens for MethodType {
115    fn to_tokens(&self, stream: &mut TokenStream2) {
116        let ident = Ident::new(self.as_str(), Span::call_site());
117        stream.append(ident);
118    }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122enum MethodTypeExt {
123    Standard(MethodType),
124    Custom(LitStr),
125}
126
127impl MethodTypeExt {
128    /// Returns a single method guard token stream.
129    fn to_tokens_single_guard(&self) -> TokenStream2 {
130        match self {
131            MethodTypeExt::Standard(method) => {
132                quote! {
133                    .guard(::actix_web::guard::#method())
134                }
135            }
136            MethodTypeExt::Custom(lit) => {
137                quote! {
138                    .guard(::actix_web::guard::Method(
139                        ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
140                    ))
141                }
142            }
143        }
144    }
145
146    /// Returns a multi-method guard chain token stream.
147    fn to_tokens_multi_guard(&self, or_chain: Vec<impl ToTokens>) -> TokenStream2 {
148        debug_assert!(
149            !or_chain.is_empty(),
150            "empty or_chain passed to multi-guard constructor"
151        );
152
153        match self {
154            MethodTypeExt::Standard(method) => {
155                quote! {
156                    .guard(
157                        ::actix_web::guard::Any(::actix_web::guard::#method())
158                            #(#or_chain)*
159                    )
160                }
161            }
162            MethodTypeExt::Custom(lit) => {
163                quote! {
164                    .guard(
165                        ::actix_web::guard::Any(
166                            ::actix_web::guard::Method(
167                                ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
168                            )
169                        )
170                        #(#or_chain)*
171                    )
172                }
173            }
174        }
175    }
176
177    /// Returns a token stream containing the `.or` chain to be passed in to
178    /// [`MethodTypeExt::to_tokens_multi_guard()`].
179    fn to_tokens_multi_guard_or_chain(&self) -> TokenStream2 {
180        match self {
181            MethodTypeExt::Standard(method_type) => {
182                quote! {
183                    .or(::actix_web::guard::#method_type())
184                }
185            }
186            MethodTypeExt::Custom(lit) => {
187                quote! {
188                    .or(
189                        ::actix_web::guard::Method(
190                            ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap()
191                        )
192                    )
193                }
194            }
195        }
196    }
197}
198
199impl ToTokens for MethodTypeExt {
200    fn to_tokens(&self, stream: &mut TokenStream2) {
201        match self {
202            MethodTypeExt::Custom(lit_str) => {
203                let ident = Ident::new(lit_str.value().as_str(), Span::call_site());
204                stream.append(ident);
205            }
206            MethodTypeExt::Standard(method) => method.to_tokens(stream),
207        }
208    }
209}
210
211impl TryFrom<&syn::LitStr> for MethodTypeExt {
212    type Error = syn::Error;
213
214    fn try_from(value: &syn::LitStr) -> Result<Self, Self::Error> {
215        match MethodType::try_from(value) {
216            Ok(method) => Ok(MethodTypeExt::Standard(method)),
217            Err(_) if value.value().chars().all(|c| c.is_ascii_uppercase()) => {
218                Ok(MethodTypeExt::Custom(value.clone()))
219            }
220            Err(err) => Err(err),
221        }
222    }
223}
224
225struct Args {
226    path: syn::LitStr,
227    resource_name: Option<syn::LitStr>,
228    guards: Vec<Path>,
229    wrappers: Vec<syn::Expr>,
230    methods: HashSet<MethodTypeExt>,
231}
232
233impl Args {
234    fn new(args: RouteArgs, method: Option<MethodType>) -> syn::Result<Self> {
235        let mut resource_name = None;
236        let mut guards = Vec::new();
237        let mut wrappers = Vec::new();
238        let mut methods = HashSet::new();
239
240        let is_route_macro = method.is_none();
241        if let Some(method) = method {
242            methods.insert(MethodTypeExt::Standard(method));
243        }
244
245        for nv in args.options {
246            if nv.path.is_ident("name") {
247                if let syn::Expr::Lit(syn::ExprLit {
248                    lit: syn::Lit::Str(lit),
249                    ..
250                }) = nv.value
251                {
252                    resource_name = Some(lit);
253                } else {
254                    return Err(syn::Error::new_spanned(
255                        nv.value,
256                        "Attribute name expects literal string",
257                    ));
258                }
259            } else if nv.path.is_ident("guard") {
260                if let syn::Expr::Lit(syn::ExprLit {
261                    lit: syn::Lit::Str(lit),
262                    ..
263                }) = nv.value
264                {
265                    guards.push(lit.parse::<Path>()?);
266                } else {
267                    return Err(syn::Error::new_spanned(
268                        nv.value,
269                        "Attribute guard expects literal string",
270                    ));
271                }
272            } else if nv.path.is_ident("wrap") {
273                if let syn::Expr::Lit(syn::ExprLit {
274                    lit: syn::Lit::Str(lit),
275                    ..
276                }) = nv.value
277                {
278                    wrappers.push(lit.parse()?);
279                } else {
280                    return Err(syn::Error::new_spanned(
281                        nv.value,
282                        "Attribute wrap expects type",
283                    ));
284                }
285            } else if nv.path.is_ident("method") {
286                if !is_route_macro {
287                    return Err(syn::Error::new_spanned(
288                        &nv,
289                        "HTTP method forbidden here; to handle multiple methods, use `route` instead",
290                    ));
291                } else if let syn::Expr::Lit(syn::ExprLit {
292                    lit: syn::Lit::Str(lit),
293                    ..
294                }) = nv.value.clone()
295                {
296                    if !methods.insert(MethodTypeExt::try_from(&lit)?) {
297                        return Err(syn::Error::new_spanned(
298                            nv.value,
299                            format!("HTTP method defined more than once: `{}`", lit.value()),
300                        ));
301                    }
302                } else {
303                    return Err(syn::Error::new_spanned(
304                        nv.value,
305                        "Attribute method expects literal string",
306                    ));
307                }
308            } else {
309                return Err(syn::Error::new_spanned(
310                    nv.path,
311                    "Unknown attribute key is specified; allowed: guard, method and wrap",
312                ));
313            }
314        }
315
316        Ok(Args {
317            path: args.path,
318            resource_name,
319            guards,
320            wrappers,
321            methods,
322        })
323    }
324}
325
326pub struct Route {
327    /// Name of the handler function being annotated.
328    name: syn::Ident,
329
330    /// Args passed to routing macro.
331    ///
332    /// When using `#[routes]`, this will contain args for each specific routing macro.
333    args: Vec<Args>,
334
335    /// AST of the handler function being annotated.
336    ast: syn::ItemFn,
337
338    /// The doc comment attributes to copy to generated struct, if any.
339    doc_attributes: Vec<syn::Attribute>,
340}
341
342impl Route {
343    pub fn new(args: RouteArgs, ast: syn::ItemFn, method: Option<MethodType>) -> syn::Result<Self> {
344        let name = ast.sig.ident.clone();
345
346        // Try and pull out the doc comments so that we can reapply them to the generated struct.
347        // Note that multi line doc comments are converted to multiple doc attributes.
348        let doc_attributes = ast
349            .attrs
350            .iter()
351            .filter(|attr| attr.path().is_ident("doc"))
352            .cloned()
353            .collect();
354
355        let args = Args::new(args, method)?;
356
357        if args.methods.is_empty() {
358            return Err(syn::Error::new(
359                Span::call_site(),
360                "The #[route(..)] macro requires at least one `method` attribute",
361            ));
362        }
363
364        if matches!(ast.sig.output, syn::ReturnType::Default) {
365            return Err(syn::Error::new_spanned(
366                ast,
367                "Function has no return type. Cannot be used as handler",
368            ));
369        }
370
371        Ok(Self {
372            name,
373            args: vec![args],
374            ast,
375            doc_attributes,
376        })
377    }
378
379    fn multiple(args: Vec<Args>, ast: syn::ItemFn) -> syn::Result<Self> {
380        let name = ast.sig.ident.clone();
381
382        // Try and pull out the doc comments so that we can reapply them to the generated struct.
383        // Note that multi line doc comments are converted to multiple doc attributes.
384        let doc_attributes = ast
385            .attrs
386            .iter()
387            .filter(|attr| attr.path().is_ident("doc"))
388            .cloned()
389            .collect();
390
391        if matches!(ast.sig.output, syn::ReturnType::Default) {
392            return Err(syn::Error::new_spanned(
393                ast,
394                "Function has no return type. Cannot be used as handler",
395            ));
396        }
397
398        Ok(Self {
399            name,
400            args,
401            ast,
402            doc_attributes,
403        })
404    }
405}
406
407impl ToTokens for Route {
408    fn to_tokens(&self, output: &mut TokenStream2) {
409        let Self {
410            name,
411            ast,
412            args,
413            doc_attributes,
414        } = self;
415
416        #[allow(unused_variables)] // used when force-pub feature is disabled
417        let vis = &ast.vis;
418
419        // TODO(breaking): remove this force-pub forwards-compatibility feature
420        #[cfg(feature = "compat-routing-macros-force-pub")]
421        let vis = syn::Visibility::Public(<Token![pub]>::default());
422
423        let registrations: TokenStream2 = args
424            .iter()
425            .map(|args| {
426                let Args {
427                    path,
428                    resource_name,
429                    guards,
430                    wrappers,
431                    methods,
432                } = args;
433
434                let resource_name = resource_name
435                    .as_ref()
436                    .map_or_else(|| name.to_string(), LitStr::value);
437
438                let method_guards = {
439                    debug_assert!(!methods.is_empty(), "Args::methods should not be empty");
440
441                    let mut others = methods.iter();
442                    let first = others.next().unwrap();
443
444                    if methods.len() > 1 {
445                        let other_method_guards = others
446                            .map(|method_ext| method_ext.to_tokens_multi_guard_or_chain())
447                            .collect();
448
449                        first.to_tokens_multi_guard(other_method_guards)
450                    } else {
451                        first.to_tokens_single_guard()
452                    }
453                };
454
455                quote! {
456                    let __resource = ::actix_web::Resource::new(#path)
457                        .name(#resource_name)
458                        #method_guards
459                        #(.guard(::actix_web::guard::fn_guard(#guards)))*
460                        #(.wrap(#wrappers))*
461                        .to(#name);
462                    ::actix_web::dev::HttpServiceFactory::register(__resource, __config);
463                }
464            })
465            .collect();
466
467        let stream = quote! {
468            #(#doc_attributes)*
469            #[allow(non_camel_case_types, missing_docs)]
470            #vis struct #name;
471
472            impl ::actix_web::dev::HttpServiceFactory for #name {
473                fn register(self, __config: &mut actix_web::dev::AppService) {
474                    #ast
475                    #registrations
476                }
477            }
478        };
479
480        output.extend(stream);
481    }
482}
483
484pub(crate) fn with_method(
485    method: Option<MethodType>,
486    args: TokenStream,
487    input: TokenStream,
488) -> TokenStream {
489    let args = match syn::parse(args) {
490        Ok(args) => args,
491        // on parse error, make IDEs happy; see fn docs
492        Err(err) => return input_and_compile_error(input, err),
493    };
494
495    let ast = match syn::parse::<syn::ItemFn>(input.clone()) {
496        Ok(ast) => ast,
497        // on parse error, make IDEs happy; see fn docs
498        Err(err) => return input_and_compile_error(input, err),
499    };
500
501    match Route::new(args, ast, method) {
502        Ok(route) => route.into_token_stream().into(),
503        // on macro related error, make IDEs happy; see fn docs
504        Err(err) => input_and_compile_error(input, err),
505    }
506}
507
508pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
509    let mut ast = match syn::parse::<syn::ItemFn>(input.clone()) {
510        Ok(ast) => ast,
511        // on parse error, make IDEs happy; see fn docs
512        Err(err) => return input_and_compile_error(input, err),
513    };
514
515    let (methods, others) = ast
516        .attrs
517        .into_iter()
518        .map(|attr| match MethodType::from_path(attr.path()) {
519            Ok(method) => Ok((method, attr)),
520            Err(_) => Err(attr),
521        })
522        .partition::<Vec<_>, _>(Result::is_ok);
523
524    ast.attrs = others.into_iter().map(Result::unwrap_err).collect();
525
526    let methods = match methods
527        .into_iter()
528        .map(Result::unwrap)
529        .map(|(method, attr)| {
530            attr.parse_args()
531                .and_then(|args| Args::new(args, Some(method)))
532        })
533        .collect::<Result<Vec<_>, _>>()
534    {
535        Ok(methods) if methods.is_empty() => {
536            return input_and_compile_error(
537                input,
538                syn::Error::new(
539                    Span::call_site(),
540                    "The #[routes] macro requires at least one `#[<method>(..)]` attribute.",
541                ),
542            )
543        }
544        Ok(methods) => methods,
545        Err(err) => return input_and_compile_error(input, err),
546    };
547
548    match Route::multiple(methods, ast) {
549        Ok(route) => route.into_token_stream().into(),
550        // on macro related error, make IDEs happy; see fn docs
551        Err(err) => input_and_compile_error(input, err),
552    }
553}