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 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 let _ = ResourceDef::new(path.value());
31
32 if !input.peek(Token![,]) {
34 return Ok(Self {
35 path,
36 options: Punctuated::new(),
37 });
38 }
39
40 input.parse::<Token![,]>()?;
42
43 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 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 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 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 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: syn::Ident,
329
330 args: Vec<Args>,
334
335 ast: syn::ItemFn,
337
338 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 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 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)] let vis = &ast.vis;
418
419 #[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 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 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 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 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 Err(err) => input_and_compile_error(input, err),
552 }
553}