Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 83 additions & 3 deletions actix-web-codegen/src/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -344,6 +347,8 @@ impl Route {
pub fn new(args: RouteArgs, ast: syn::ItemFn, method: Option<MethodType>) -> syn::Result<Self> {
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
Expand All @@ -370,6 +375,7 @@ impl Route {
}

Ok(Self {
generics,
name,
args: vec![args],
ast,
Expand All @@ -380,6 +386,8 @@ impl Route {
fn multiple(args: Vec<Args>, ast: syn::ItemFn) -> syn::Result<Self> {
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
Expand All @@ -398,6 +406,7 @@ impl Route {

Ok(Self {
name,
generics,
args,
ast,
doc_attributes,
Expand All @@ -409,6 +418,7 @@ impl ToTokens for Route {
fn to_tokens(&self, output: &mut TokenStream2) {
let Self {
name,
generics,
ast,
args,
doc_attributes,
Expand All @@ -421,6 +431,54 @@ impl ToTokens for Route {
#[cfg(feature = "compat-routing-macros-force-pub")]
let vis = syn::Visibility::Public(<Token![pub]>::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<TokenStream2> = generics
.params
.iter()
.map(|param| match param {
syn::GenericParam::Type(ty) => {
let ident = &ty.ident;
quote! { #ident }
}
syn::GenericParam::Lifetime(lt) => {
let lifetime = &lt.lifetime;
quote! { &#lifetime () }
}
syn::GenericParam::Const(konst) => {
let ident = &konst.ident;
quote! { [(); #ident] }
}
})
.collect();

let phantom_tuple = quote! { (#(#phantom_args, )*) };

let turbofish_args: Vec<TokenStream2> = 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| {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions actix-web-codegen/tests/trybuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
50 changes: 50 additions & 0 deletions actix-web-codegen/tests/trybuild/route-generic-ok.rs
Original file line number Diff line number Diff line change
@@ -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<T>(client: web::Data<T>) -> String
where
T: UserRepository + Send + Sync + 'static,
{
client.get_ref().get_user().to_string()
}

#[get("/multi")]
async fn multi<T, U>(client: web::Data<T>, _flag: web::Data<U>) -> 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<const N: usize>() -> String {
format!("{N}")
}

fn main() {
let app = App::new()
.app_data(web::Data::new(UserClient))
.app_data(web::Data::new(Flag))
.service(index::<UserClient>::default())
.service(multi::<UserClient, Flag>::default())
.service(with_const::<3>::default());

let _ = app;
}
Loading