diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index cd1ad4c6639..b56700f67e9 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -328,6 +328,9 @@ pub struct Route { /// Name of the handler function being annotated. name: syn::Ident, + /// Handler function generics. + generics: syn::Generics, + /// Args passed to routing macro. /// /// When using `#[routes]`, this will contain args for each specific routing macro. @@ -344,6 +347,8 @@ impl Route { pub fn new(args: RouteArgs, ast: syn::ItemFn, method: Option) -> syn::Result { let name = ast.sig.ident.clone(); + let generics = ast.sig.generics.clone(); + // Try and pull out the doc comments so that we can reapply them to the generated struct. // Note that multi line doc comments are converted to multiple doc attributes. let doc_attributes = ast @@ -370,6 +375,7 @@ impl Route { } Ok(Self { + generics, name, args: vec![args], ast, @@ -380,6 +386,8 @@ impl Route { fn multiple(args: Vec, ast: syn::ItemFn) -> syn::Result { let name = ast.sig.ident.clone(); + let generics = ast.sig.generics.clone(); + // Try and pull out the doc comments so that we can reapply them to the generated struct. // Note that multi line doc comments are converted to multiple doc attributes. let doc_attributes = ast @@ -398,6 +406,7 @@ impl Route { Ok(Self { name, + generics, args, ast, doc_attributes, @@ -409,6 +418,7 @@ impl ToTokens for Route { fn to_tokens(&self, output: &mut TokenStream2) { let Self { name, + generics, ast, args, doc_attributes, @@ -421,6 +431,54 @@ impl ToTokens for Route { #[cfg(feature = "compat-routing-macros-force-pub")] let vis = syn::Visibility::Public(::default()); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let mut struct_generics = generics.clone(); + struct_generics.where_clause = None; + + let phantom_args: Vec = generics + .params + .iter() + .map(|param| match param { + syn::GenericParam::Type(ty) => { + let ident = &ty.ident; + quote! { #ident } + } + syn::GenericParam::Lifetime(lt) => { + let lifetime = <.lifetime; + quote! { &#lifetime () } + } + syn::GenericParam::Const(konst) => { + let ident = &konst.ident; + quote! { [(); #ident] } + } + }) + .collect(); + + let phantom_tuple = quote! { (#(#phantom_args, )*) }; + + let turbofish_args: Vec = generics + .params + .iter() + .filter_map(|param| match param { + syn::GenericParam::Type(ty) => { + let ident = &ty.ident; + Some(quote! { #ident }) + } + syn::GenericParam::Const(konst) => { + let ident = &konst.ident; + Some(quote! { #ident }) + } + syn::GenericParam::Lifetime(_) => None, + }) + .collect(); + + let turbofish = if turbofish_args.is_empty() { + TokenStream2::new() + } else { + quote! { ::<#(#turbofish_args),*> } + }; + let registrations: TokenStream2 = args .iter() .map(|args| { @@ -459,18 +517,40 @@ impl ToTokens for Route { #method_guards #(.guard(::actix_web::guard::fn_guard(#guards)))* #(.wrap(#wrappers))* - .to(#name); + .to(#name #turbofish); ::actix_web::dev::HttpServiceFactory::register(__resource, __config); } }) .collect(); + let struct_def = if generics.params.is_empty() { + quote! { #vis struct #name; } + } else { + quote! { + #vis struct #name #struct_generics (core::marker::PhantomData<#phantom_tuple>); + } + }; + + let default_expr = if generics.params.is_empty() { + quote! { Self } + } else { + quote! { Self(core::marker::PhantomData) } + }; + let stream = quote! { #(#doc_attributes)* #[allow(non_camel_case_types)] - #vis struct #name; + #struct_def + + impl #impl_generics ::core::default::Default for #name #ty_generics { + fn default() -> Self { + #default_expr + } + } - impl ::actix_web::dev::HttpServiceFactory for #name { + impl #impl_generics ::actix_web::dev::HttpServiceFactory for #name #ty_generics + #where_clause + { fn register(self, __config: &mut actix_web::dev::AppService) { #ast #registrations diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 0150d56f299..6e63ee4e732 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -7,6 +7,7 @@ fn compile_macros() { t.compile_fail("tests/trybuild/simple-fail.rs"); t.pass("tests/trybuild/route-ok.rs"); + t.pass("tests/trybuild/route-generic-ok.rs"); t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); diff --git a/actix-web-codegen/tests/trybuild/route-generic-ok.rs b/actix-web-codegen/tests/trybuild/route-generic-ok.rs new file mode 100644 index 00000000000..651ce5c1fe3 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-generic-ok.rs @@ -0,0 +1,50 @@ +use actix_web::{get, web, App}; + +trait UserRepository { + fn get_user(&self) -> u64; +} + +#[derive(Clone)] +struct UserClient; + +impl UserRepository for UserClient { + fn get_user(&self) -> u64 { + 99 + } +} + +#[derive(Clone)] +struct Flag; + +#[get("/")] +async fn index(client: web::Data) -> String +where + T: UserRepository + Send + Sync + 'static, +{ + client.get_ref().get_user().to_string() +} + +#[get("/multi")] +async fn multi(client: web::Data, _flag: web::Data) -> String +where + T: UserRepository + Send + Sync + 'static, + U: Clone + Send + Sync + 'static, +{ + client.get_ref().get_user().to_string() +} + +#[get("/const")] +async fn with_const() -> String { + format!("{N}") +} + +fn main() { + let app = App::new() + .app_data(web::Data::new(UserClient)) + .app_data(web::Data::new(Flag)) + .service(index::::default()) + .service(multi::::default()) + .service(with_const::<3>::default()); + + let _ = app; +}