From 390555bdb6d704519c459799027bc266eba70b23 Mon Sep 17 00:00:00 2001 From: uraneko Date: Fri, 26 Sep 2025 07:26:14 +0100 Subject: [PATCH 1/2] 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/2] 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::*", ]