From 390555bdb6d704519c459799027bc266eba70b23 Mon Sep 17 00:00:00 2001 From: uraneko Date: Fri, 26 Sep 2025 07:26:14 +0100 Subject: [PATCH 1/4] add function single generic type support --- actix-web-codegen/src/route.rs | 44 ++++++++++++++-- actix-web/Cargo.toml | 96 ++++++++++++++++++---------------- actix-web/examples/2866.rs | 60 +++++++++++++++++++++ 3 files changed, 151 insertions(+), 49 deletions(-) create mode 100644 actix-web/examples/2866.rs diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index cd1ad4c66..7cea54e11 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, + /// function generic + type_generic: Option, + /// Args passed to routing macro. /// /// When using `#[routes]`, this will contain args for each specific routing macro. @@ -344,6 +347,13 @@ 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.params.clone(); + let type_generic = if let Some(syn::GenericParam::Type(ty)) = generics.into_iter().next() { + Some(ty) + } else { + None + }; + // 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 +380,7 @@ impl Route { } Ok(Self { + type_generic, name, args: vec![args], ast, @@ -380,6 +391,13 @@ impl Route { fn multiple(args: Vec, ast: syn::ItemFn) -> syn::Result { let name = ast.sig.ident.clone(); + let generics = ast.sig.generics.params.clone(); + let type_generic = if let Some(syn::GenericParam::Type(ty)) = generics.into_iter().next() { + Some(ty) + } else { + None + }; + // 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 +416,7 @@ impl Route { Ok(Self { name, + type_generic, args, ast, doc_attributes, @@ -409,6 +428,7 @@ impl ToTokens for Route { fn to_tokens(&self, output: &mut TokenStream2) { let Self { name, + type_generic, ast, args, doc_attributes, @@ -421,6 +441,17 @@ impl ToTokens for Route { #[cfg(feature = "compat-routing-macros-force-pub")] let vis = syn::Visibility::Public(::default()); + let (struct_generic, trait_generic, impl_type_generic) = + if let Some(syn::TypeParam { ident, bounds, .. }) = type_generic { + ( + Some(quote! { <#ident> (core::marker::PhantomData) }), + Some(quote! { <#ident: #bounds + 'static> }), + Some(quote! { <#ident> }), + ) + } else { + (None, None, None) + }; + let registrations: TokenStream2 = args .iter() .map(|args| { @@ -453,13 +484,19 @@ impl ToTokens for Route { } }; + let type_generic = if let Some(syn::TypeParam { ident, .. }) = type_generic { + Some(quote! { ::<#ident> }) + } else { + None + }; + quote! { let __resource = ::actix_web::Resource::new(#path) .name(#resource_name) #method_guards #(.guard(::actix_web::guard::fn_guard(#guards)))* #(.wrap(#wrappers))* - .to(#name); + .to(#name #type_generic); ::actix_web::dev::HttpServiceFactory::register(__resource, __config); } }) @@ -468,9 +505,10 @@ impl ToTokens for Route { let stream = quote! { #(#doc_attributes)* #[allow(non_camel_case_types)] - #vis struct #name; + #[derive(Default)] + #vis struct #name #struct_generic; - impl ::actix_web::dev::HttpServiceFactory for #name { + impl #trait_generic ::actix_web::dev::HttpServiceFactory for #name #impl_type_generic { fn register(self, __config: &mut actix_web::dev::AppService) { #ast #registrations diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index e6eea5da4..1cad115d8 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -5,10 +5,10 @@ description = "Actix Web is a powerful, pragmatic, and extremely fast web framew authors = ["Nikolay Kim ", "Rob Ede "] keywords = ["actix", "http", "web", "framework", "async"] categories = [ - "network-programming", - "asynchronous", - "web-programming::http-server", - "web-programming::websocket", + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", ] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" @@ -18,56 +18,56 @@ rust-version.workspace = true [package.metadata.docs.rs] features = [ - "macros", - "openssl", - "rustls-0_20", - "rustls-0_21", - "rustls-0_22", - "rustls-0_23", - "compress-brotli", - "compress-gzip", - "compress-zstd", - "cookies", - "secure-cookies", + "macros", + "openssl", + "rustls-0_20", + "rustls-0_21", + "rustls-0_22", + "rustls-0_23", + "compress-brotli", + "compress-gzip", + "compress-zstd", + "cookies", + "secure-cookies", ] [package.metadata.cargo_check_external_types] allowed_external_types = [ - "actix_http::*", - "actix_router::*", - "actix_rt::*", - "actix_server::*", - "actix_service::*", - "actix_utils::*", - "actix_web_codegen::*", - "bytes::*", - "cookie::*", - "cookie", - "futures_core::*", - "http::*", - "language_tags::*", - "mime::*", - "openssl::*", - "rustls::*", - "serde_json::*", - "serde_urlencoded::*", - "serde::*", - "serde::*", - "tokio::*", - "url::*", + "actix_http::*", + "actix_router::*", + "actix_rt::*", + "actix_server::*", + "actix_service::*", + "actix_utils::*", + "actix_web_codegen::*", + "bytes::*", + "cookie::*", + "cookie", + "futures_core::*", + "http::*", + "language_tags::*", + "mime::*", + "openssl::*", + "rustls::*", + "serde_json::*", + "serde_urlencoded::*", + "serde::*", + "serde::*", + "tokio::*", + "url::*", ] [features] default = [ - "macros", - "compress-brotli", - "compress-gzip", - "compress-zstd", - "cookies", - "http2", - "unicode", - "compat", - "ws", + "macros", + "compress-brotli", + "compress-gzip", + "compress-zstd", + "cookies", + "http2", + "unicode", + "compat", + "ws", ] # Brotli algorithm content-encoding support @@ -204,6 +204,10 @@ required-features = ["compress-brotli", "compress-gzip", "compress-zstd"] name = "basic" required-features = ["compress-gzip"] +[[example]] +name = "issue" +path = "examples/2866.rs" + [[example]] name = "uds" required-features = ["compress-gzip"] diff --git a/actix-web/examples/2866.rs b/actix-web/examples/2866.rs new file mode 100644 index 000000000..696dadc9d --- /dev/null +++ b/actix-web/examples/2866.rs @@ -0,0 +1,60 @@ +use actix_web::{ + dev::Server, + get, + web::{self, Data}, + App, HttpServer, Responder, +}; +use serde::Serialize; + +#[derive(Debug, Serialize, Clone, Copy)] +pub struct User { + id: u64, +} + +pub trait UserRepository { + fn get_user(&self) -> User; +} + +#[derive(Clone)] +struct UserClient; + +impl UserRepository for UserClient { + fn get_user(&self) -> User { + User { id: 99 } + } +} + +// when uncommenting following the line, the type checking is unaccepted +// because of cannot infer type parameter T +#[get("/")] +async fn index(client: web::Data) -> impl Responder { + let user = client.into_inner().get_user(); + web::Json(user) +} + +#[get("hello/{who}")] +async fn hello(who: web::Path) -> impl Responder { + format!("

hello {who}

") +} + +pub fn create_server( + search: T, +) -> Result { + let server = HttpServer::new(move || { + App::new() + .app_data(Data::new(search.clone())) + // .route("/", web::get().to(index::)) + .service(index::(core::marker::PhantomData::)) + .service(hello) + }) + .bind("127.0.0.1:8080")? + .run(); + Ok(server) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + println!("\x1b[1;2;36mserving on http://localhost:8080\x1b[0m"); + let user_client = UserClient; + create_server(user_client).unwrap().await +} From 89527c5ec5c6d69688aaad5702c98ac0cb9ba6cc Mon Sep 17 00:00:00 2001 From: uraneko Date: Fri, 26 Sep 2025 08:36:18 +0100 Subject: [PATCH 2/4] resolve conflict (removes line added by mistake) --- actix-web-codegen/src/route.rs | 3 ++- actix-web/Cargo.toml | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 7cea54e11..19b0aeaf8 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -508,7 +508,8 @@ impl ToTokens for Route { #[derive(Default)] #vis struct #name #struct_generic; - impl #trait_generic ::actix_web::dev::HttpServiceFactory for #name #impl_type_generic { + impl #trait_generic ::actix_web::dev::HttpServiceFactory for #name #impl_type_generic + { fn register(self, __config: &mut actix_web::dev::AppService) { #ast #registrations diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 1cad115d8..6c5291df4 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -52,7 +52,6 @@ allowed_external_types = [ "serde_json::*", "serde_urlencoded::*", "serde::*", - "serde::*", "tokio::*", "url::*", ] From bea1e5ca4a73b04878b9320a4669a9271db3a224 Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Thu, 5 Feb 2026 15:53:55 +0900 Subject: [PATCH 3/4] follow-up --- actix-web-codegen/src/route.rs | 103 ++++++++++++------ actix-web-codegen/tests/trybuild.rs | 1 + .../tests/trybuild/route-generic-ok.rs | 50 +++++++++ actix-web/Cargo.toml | 52 ++++----- actix-web/examples/2866.rs | 60 ---------- 5 files changed, 142 insertions(+), 124 deletions(-) create mode 100644 actix-web-codegen/tests/trybuild/route-generic-ok.rs delete mode 100644 actix-web/examples/2866.rs diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 19b0aeaf8..0bc803560 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -328,8 +328,8 @@ pub struct Route { /// Name of the handler function being annotated. name: syn::Ident, - /// function generic - type_generic: Option, + /// Handler function generics. + generics: syn::Generics, /// Args passed to routing macro. /// @@ -347,12 +347,7 @@ 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.params.clone(); - let type_generic = if let Some(syn::GenericParam::Type(ty)) = generics.into_iter().next() { - Some(ty) - } else { - None - }; + 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. @@ -380,7 +375,7 @@ impl Route { } Ok(Self { - type_generic, + generics, name, args: vec![args], ast, @@ -391,12 +386,7 @@ impl Route { fn multiple(args: Vec, ast: syn::ItemFn) -> syn::Result { let name = ast.sig.ident.clone(); - let generics = ast.sig.generics.params.clone(); - let type_generic = if let Some(syn::GenericParam::Type(ty)) = generics.into_iter().next() { - Some(ty) - } else { - None - }; + 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. @@ -416,7 +406,7 @@ impl Route { Ok(Self { name, - type_generic, + generics, args, ast, doc_attributes, @@ -428,7 +418,7 @@ impl ToTokens for Route { fn to_tokens(&self, output: &mut TokenStream2) { let Self { name, - type_generic, + generics, ast, args, doc_attributes, @@ -441,16 +431,53 @@ impl ToTokens for Route { #[cfg(feature = "compat-routing-macros-force-pub")] let vis = syn::Visibility::Public(::default()); - let (struct_generic, trait_generic, impl_type_generic) = - if let Some(syn::TypeParam { ident, bounds, .. }) = type_generic { - ( - Some(quote! { <#ident> (core::marker::PhantomData) }), - Some(quote! { <#ident: #bounds + 'static> }), - Some(quote! { <#ident> }), - ) - } else { - (None, None, None) - }; + 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() @@ -484,31 +511,35 @@ impl ToTokens for Route { } }; - let type_generic = if let Some(syn::TypeParam { ident, .. }) = type_generic { - Some(quote! { ::<#ident> }) - } else { - None - }; - quote! { let __resource = ::actix_web::Resource::new(#path) .name(#resource_name) #method_guards #(.guard(::actix_web::guard::fn_guard(#guards)))* #(.wrap(#wrappers))* - .to(#name #type_generic); + .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>) + #where_clause; + } + }; + let stream = quote! { #(#doc_attributes)* #[allow(non_camel_case_types)] #[derive(Default)] - #vis struct #name #struct_generic; + #struct_def - impl #trait_generic ::actix_web::dev::HttpServiceFactory for #name #impl_type_generic + impl #impl_generics ::actix_web::dev::HttpServiceFactory for #name #ty_generics + #where_clause { fn register(self, __config: &mut actix_web::dev::AppService) { #ast diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 0150d56f2..6e63ee4e7 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 000000000..651ce5c1f --- /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; +} diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index bd4f10a15..085e89371 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -5,10 +5,10 @@ description = "Actix Web is a powerful, pragmatic, and extremely fast web framew authors = ["Nikolay Kim ", "Rob Ede "] keywords = ["actix", "http", "web", "framework", "async"] categories = [ - "network-programming", - "asynchronous", - "web-programming::http-server", - "web-programming::websocket", + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", ] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" @@ -18,17 +18,17 @@ rust-version.workspace = true [package.metadata.docs.rs] features = [ - "macros", - "openssl", - "rustls-0_20", - "rustls-0_21", - "rustls-0_22", - "rustls-0_23", - "compress-brotli", - "compress-gzip", - "compress-zstd", - "cookies", - "secure-cookies", + "macros", + "openssl", + "rustls-0_20", + "rustls-0_21", + "rustls-0_22", + "rustls-0_23", + "compress-brotli", + "compress-gzip", + "compress-zstd", + "cookies", + "secure-cookies", ] [package.metadata.cargo_check_external_types] @@ -58,15 +58,15 @@ allowed_external_types = [ [features] default = [ - "macros", - "compress-brotli", - "compress-gzip", - "compress-zstd", - "cookies", - "http2", - "unicode", - "compat", - "ws", + "macros", + "compress-brotli", + "compress-gzip", + "compress-zstd", + "cookies", + "http2", + "unicode", + "compat", + "ws", ] # Brotli algorithm content-encoding support @@ -203,10 +203,6 @@ required-features = ["compress-brotli", "compress-gzip", "compress-zstd"] name = "basic" required-features = ["compress-gzip"] -[[example]] -name = "issue" -path = "examples/2866.rs" - [[example]] name = "uds" required-features = ["compress-gzip"] diff --git a/actix-web/examples/2866.rs b/actix-web/examples/2866.rs deleted file mode 100644 index 696dadc9d..000000000 --- a/actix-web/examples/2866.rs +++ /dev/null @@ -1,60 +0,0 @@ -use actix_web::{ - dev::Server, - get, - web::{self, Data}, - App, HttpServer, Responder, -}; -use serde::Serialize; - -#[derive(Debug, Serialize, Clone, Copy)] -pub struct User { - id: u64, -} - -pub trait UserRepository { - fn get_user(&self) -> User; -} - -#[derive(Clone)] -struct UserClient; - -impl UserRepository for UserClient { - fn get_user(&self) -> User { - User { id: 99 } - } -} - -// when uncommenting following the line, the type checking is unaccepted -// because of cannot infer type parameter T -#[get("/")] -async fn index(client: web::Data) -> impl Responder { - let user = client.into_inner().get_user(); - web::Json(user) -} - -#[get("hello/{who}")] -async fn hello(who: web::Path) -> impl Responder { - format!("

hello {who}

") -} - -pub fn create_server( - search: T, -) -> Result { - let server = HttpServer::new(move || { - App::new() - .app_data(Data::new(search.clone())) - // .route("/", web::get().to(index::)) - .service(index::(core::marker::PhantomData::)) - .service(hello) - }) - .bind("127.0.0.1:8080")? - .run(); - Ok(server) -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - println!("\x1b[1;2;36mserving on http://localhost:8080\x1b[0m"); - let user_client = UserClient; - create_server(user_client).unwrap().await -} From 64946ac15c5e8ee957a48d05c536c8faf53d966c Mon Sep 17 00:00:00 2001 From: Yuki Okushi Date: Thu, 5 Feb 2026 16:24:52 +0900 Subject: [PATCH 4/4] make it work on msrv --- actix-web-codegen/src/route.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 0bc803560..b56700f67 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -527,17 +527,27 @@ impl ToTokens for Route { quote! { #vis struct #name; } } else { quote! { - #vis struct #name #struct_generics (core::marker::PhantomData<#phantom_tuple>) - #where_clause; + #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)] - #[derive(Default)] #struct_def + impl #impl_generics ::core::default::Default for #name #ty_generics { + fn default() -> Self { + #default_expr + } + } + impl #impl_generics ::actix_web::dev::HttpServiceFactory for #name #ty_generics #where_clause {