From 56f1ce863a6e405c9e325469e0bf3db5e93b8e18 Mon Sep 17 00:00:00 2001 From: Rakshith Ravi Date: Sun, 22 Sep 2024 10:52:31 +0530 Subject: [PATCH 01/11] WIP added generic types for server fns --- examples/server_fns_axum/src/app.rs | 15 ++++++++- server_fn_macro/src/lib.rs | 52 ++++++++++++++++++----------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 75f5c8fe79..7146925a0d 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -12,12 +12,12 @@ use server_fn::{ request::{browser::BrowserRequest, ClientReq, Req}, response::{browser::BrowserResponse, ClientRes, Res}, }; -use std::future::Future; #[cfg(feature = "ssr")] use std::sync::{ atomic::{AtomicU8, Ordering}, Mutex, }; +use std::{fmt::Display, future::Future}; use strum::{Display, EnumString}; use wasm_bindgen::JsCast; use web_sys::{FormData, HtmlFormElement, SubmitEvent}; @@ -782,6 +782,19 @@ pub struct WhyNotResult { modified: String, } +#[server] +pub async fn test_fn( + first: S, + second: S, +) -> Result +where + S: Display + Send + DeserializeOwned + Serialize + 'static, +{ + let original = first.to_string(); + let modified = format!("{original}{second}"); + Ok(WhyNotResult { original, modified }) +} + #[server( input = Toml, output = Toml, diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 1e80222a8e..043f081498 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -242,6 +242,11 @@ pub fn server_macro_impl( #server_fn_path::codec::Json } }); + + let (impl_generics, ty_generics, where_clause) = + body.generics.split_for_impl(); + let turbofish_ty_generics = ty_generics.as_turbofish(); + // default to PascalCase version of function name if no struct name given let struct_name = struct_name.unwrap_or_else(|| { let upper_camel_case_name = Converter::new() @@ -253,15 +258,15 @@ pub fn server_macro_impl( // struct name, wrapped in any custom-encoding newtype wrapper let wrapped_struct_name = if let Some(wrapper) = custom_wrapper.as_ref() { - quote! { #wrapper<#struct_name> } + quote! { #wrapper::<#struct_name #ty_generics> } } else { - quote! { #struct_name } + quote! { #struct_name #ty_generics } }; let wrapped_struct_name_turbofish = if let Some(wrapper) = custom_wrapper.as_ref() { - quote! { #wrapper::<#struct_name> } + quote! { #wrapper::<#struct_name #ty_generics> } } else { - quote! { #struct_name } + quote! { #struct_name #ty_generics } }; // build struct for type @@ -301,16 +306,16 @@ pub fn server_macro_impl( let field = first_field.unwrap(); let (name, ty) = field; quote! { - impl From<#struct_name> for #ty { - fn from(value: #struct_name) -> Self { - let #struct_name { #name } = value; + impl #impl_generics From<#struct_name #ty_generics> for #ty #where_clause { + fn from(value: #struct_name #ty_generics) -> Self { + let #struct_name #turbofish_ty_generics { #name } = value; #name } } - impl From<#ty> for #struct_name { + impl #impl_generics From<#ty> for #struct_name #ty_generics #where_clause { fn from(#name: #ty) -> Self { - #struct_name { #name } + #struct_name #turbofish_ty_generics { #name } } } } @@ -386,11 +391,11 @@ pub fn server_macro_impl( let run_body = if cfg!(feature = "ssr") { let destructure = if let Some(wrapper) = custom_wrapper.as_ref() { quote! { - let #wrapper(#struct_name { #(#field_names),* }) = self; + let #wrapper(#struct_name #turbofish_ty_generics { #(#field_names),* }) = self; } } else { quote! { - let #struct_name { #(#field_names),* } = self; + let #struct_name #turbofish_ty_generics { #(#field_names),* } = self; } }; @@ -439,7 +444,7 @@ pub fn server_macro_impl( quote! { #docs #(#attrs)* - #vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty { + #vis async fn #fn_name #ty_generics (#(#fn_args),*) #output_arrow #return_ty #where_clause { #dummy_name(#(#field_names),*).await } } @@ -447,18 +452,18 @@ pub fn server_macro_impl( let restructure = if let Some(custom_wrapper) = custom_wrapper.as_ref() { quote! { - let data = #custom_wrapper(#struct_name { #(#field_names),* }); + let data = #custom_wrapper(#struct_name #turbofish_ty_generics { #(#field_names),* }); } } else { quote! { - let data = #struct_name { #(#field_names),* }; + let data = #struct_name #turbofish_ty_generics { #(#field_names),* }; } }; quote! { #docs #(#attrs)* #[allow(unused_variables)] - #vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty { + #vis async fn #fn_name #ty_generics (#(#fn_args),*) #output_arrow #return_ty #where_clause { use #server_fn_path::ServerFn; #restructure data.run_on_client().await @@ -628,18 +633,18 @@ pub fn server_macro_impl( quote! { vec![] } }; - Ok(quote::quote! { + let quote = quote::quote! { #args_docs #docs #[derive(Debug, #derives)] #addl_path - pub struct #struct_name { + pub struct #struct_name #ty_generics #where_clause { #(#fields),* } #from_impl - impl #server_fn_path::ServerFn for #wrapped_struct_name { + impl #impl_generics #server_fn_path::ServerFn for #wrapped_struct_name #where_clause { const PATH: &'static str = #path; type Client = #client; @@ -662,7 +667,13 @@ pub fn server_macro_impl( #func #dummy - }) + }; + + if !ty_generics.into_token_stream().is_empty() { + println!("{}", quote); + } + + Ok(quote) } fn type_from_ident(ident: Ident) -> Type { @@ -1045,7 +1056,7 @@ impl Parse for ServerFnBody { let fn_token = input.parse()?; let ident = input.parse()?; - let generics: Generics = input.parse()?; + let mut generics = input.parse::()?; let content; let _paren_token = syn::parenthesized!(content in input); @@ -1054,6 +1065,7 @@ impl Parse for ServerFnBody { let output_arrow = input.parse()?; let return_ty = input.parse()?; + generics.where_clause = input.parse()?; let block = input.parse()?; From 42cb3cd8018125413a549807267da38b875a40fa Mon Sep 17 00:00:00 2001 From: Rakshith Ravi Date: Sun, 22 Sep 2024 12:25:38 +0530 Subject: [PATCH 02/11] Fixed extra serde bounds --- examples/server_fns_axum/src/app.rs | 2 +- server_fn_macro/src/lib.rs | 101 +++++++++++++++++++++++++--- 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 7146925a0d..15485169c3 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -788,7 +788,7 @@ pub async fn test_fn( second: S, ) -> Result where - S: Display + Send + DeserializeOwned + Serialize + 'static, + S: Display, { let original = first.to_string(); let modified = format!("{original}{second}"); diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 043f081498..2523e06e06 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -247,6 +247,94 @@ pub fn server_macro_impl( body.generics.split_for_impl(); let turbofish_ty_generics = ty_generics.as_turbofish(); + // For the struct declaration, add a where clause where serde::Serialize + serde::DeserializeOwned is not required + let struct_decl_where_clause = + where_clause.cloned().map(|mut where_clause| { + where_clause.predicates = where_clause + .predicates + .into_iter() + .map(|predicate| { + if let WherePredicate::Type(mut t) = predicate { + // Check if the type is used in the struct + let is_type_used = + body.inputs.iter().any(|f| match f { + FnArg::Receiver(_) => false, + FnArg::Typed(typed) => { + *typed.ty == t.bounded_ty + } + }); + + if is_type_used { + // If the type is used in the struct, add the bounds + t.bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: syn::parse_quote!(Send), + })); + t.bounds.push(TypeParamBound::Lifetime( + syn::parse_quote!('static), + )); + } + WherePredicate::Type(t) + } else { + predicate + } + }) + .collect(); + where_clause + }); + + // Add a `: Serialize + for<'leptos_lifetime_param> Deserialize<'leptos_lifetime_param> + Send + 'static` bound to all types that are used in the struct + let where_clause = where_clause.cloned().map(|mut where_clause| { + where_clause.predicates = where_clause + .predicates + .into_iter() + .map(|predicate| { + if let WherePredicate::Type(mut t) = predicate { + // Check if the type is used in the struct + let is_type_used = body.inputs.iter().any(|f| match f { + FnArg::Receiver(_) => false, + FnArg::Typed(typed) => *typed.ty == t.bounded_ty, + }); + + if is_type_used { + // If the type is used in the struct, add the bounds + t.bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: syn::parse_quote!( + #server_fn_path::serde::Serialize + ), + })); + t.bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: Some(syn::parse_quote!(for<'leptos_param_lifetime>)), + path: syn::parse_quote!( + #server_fn_path::serde::Deserialize::<'leptos_param_lifetime> + ), + })); + t.bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: syn::parse_quote!(Send), + })); + t.bounds.push(TypeParamBound::Lifetime( + syn::parse_quote!('static), + )); + } + WherePredicate::Type(t) + } else { + predicate + } + }) + .collect(); + where_clause + }); + // default to PascalCase version of function name if no struct name given let struct_name = struct_name.unwrap_or_else(|| { let upper_camel_case_name = Converter::new() @@ -264,9 +352,9 @@ pub fn server_macro_impl( }; let wrapped_struct_name_turbofish = if let Some(wrapper) = custom_wrapper.as_ref() { - quote! { #wrapper::<#struct_name #ty_generics> } + quote! { #wrapper::<#struct_name #turbofish_ty_generics> } } else { - quote! { #struct_name #ty_generics } + quote! { #struct_name #turbofish_ty_generics } }; // build struct for type @@ -638,7 +726,7 @@ pub fn server_macro_impl( #docs #[derive(Debug, #derives)] #addl_path - pub struct #struct_name #ty_generics #where_clause { + pub struct #struct_name #ty_generics #struct_decl_where_clause { #(#fields),* } @@ -669,10 +757,6 @@ pub fn server_macro_impl( #dummy }; - if !ty_generics.into_token_stream().is_empty() { - println!("{}", quote); - } - Ok(quote) } @@ -1133,10 +1217,11 @@ impl ServerFnBody { block, .. } = &self; + let where_clause = generics.where_clause.as_ref(); quote! { #[doc(hidden)] #(#attrs)* - #vis #async_token #fn_token #ident #generics ( #inputs ) #output_arrow #return_ty + #vis #async_token #fn_token #ident #generics ( #inputs ) #output_arrow #return_ty #where_clause #block } } From f829ae851b28a651434065ec0218eb1576346305 Mon Sep 17 00:00:00 2001 From: Rakshith Ravi Date: Sun, 22 Sep 2024 12:28:15 +0530 Subject: [PATCH 03/11] Fixed comment --- server_fn_macro/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 2523e06e06..70376c77aa 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -247,7 +247,7 @@ pub fn server_macro_impl( body.generics.split_for_impl(); let turbofish_ty_generics = ty_generics.as_turbofish(); - // For the struct declaration, add a where clause where serde::Serialize + serde::DeserializeOwned is not required + // For the struct declaration, add a where clause where all the fields in the struct have a : Send + 'static bound let struct_decl_where_clause = where_clause.cloned().map(|mut where_clause| { where_clause.predicates = where_clause From 005cc214a1328cd96ed91ce9751284c8b5703f85 Mon Sep 17 00:00:00 2001 From: Rakshith Ravi Date: Mon, 23 Sep 2024 06:37:37 +0530 Subject: [PATCH 04/11] refactor server_fn_macro to use url() instead of PATH --- leptos_macro/tests/server.rs | 16 +++--- leptos_server/src/action.rs | 4 +- server_fn/src/lib.rs | 21 +++----- server_fn_macro/src/lib.rs | 100 +++++++++++++++++++---------------- 4 files changed, 72 insertions(+), 69 deletions(-) diff --git a/leptos_macro/tests/server.rs b/leptos_macro/tests/server.rs index 2761da8cf2..92193c4776 100644 --- a/leptos_macro/tests/server.rs +++ b/leptos_macro/tests/server.rs @@ -14,7 +14,7 @@ pub mod tests { Ok(()) } assert_eq!( - ::PATH + ::url() .trim_end_matches(char::is_numeric), "/api/my_server_action" ); @@ -30,7 +30,7 @@ pub mod tests { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(::PATH, "/foo/bar/my_path"); + assert_eq!(::url(), "/foo/bar/my_path"); assert_eq!( TypeId::of::<::InputEncoding>(), TypeId::of::() @@ -43,7 +43,7 @@ pub mod tests { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(::PATH, "/foo/bar/my_path"); + assert_eq!(::url(), "/foo/bar/my_path"); assert_eq!( TypeId::of::<::InputEncoding>(), TypeId::of::() @@ -56,7 +56,7 @@ pub mod tests { pub async fn my_server_action() -> Result<(), ServerFnError> { Ok(()) } - assert_eq!(::PATH, "/api/my_path"); + assert_eq!(::url(), "/api/my_path"); assert_eq!( TypeId::of::<::InputEncoding>(), TypeId::of::() @@ -70,7 +70,7 @@ pub mod tests { Ok(()) } assert_eq!( - ::PATH.trim_end_matches(char::is_numeric), + ::url().trim_end_matches(char::is_numeric), "/api/my_server_action" ); assert_eq!( @@ -86,7 +86,7 @@ pub mod tests { Ok(()) } assert_eq!( - ::PATH + ::url() .trim_end_matches(char::is_numeric), "/foo/bar/my_server_action" ); @@ -103,7 +103,7 @@ pub mod tests { Ok(()) } assert_eq!( - ::PATH + ::url() .trim_end_matches(char::is_numeric), "/api/my_server_action" ); @@ -120,7 +120,7 @@ pub mod tests { Ok(()) } assert_eq!( - ::PATH, + ::url(), "/api/path/to/my/endpoint" ); assert_eq!( diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index 177fb9cff1..92b45bdf90 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -57,7 +57,7 @@ where #[track_caller] pub fn new() -> Self { let err = use_context::().and_then(|error| { - (error.path() == S::PATH) + (error.path() == S::url()) .then(|| ServerFnError::::de(error.err())) .map(Err) }); @@ -145,7 +145,7 @@ where /// Creates a new [`Action`] that will call the server function `S` when dispatched. pub fn new() -> Self { let err = use_context::().and_then(|error| { - (error.path() == S::PATH) + (error.path() == S::url()) .then(|| ServerFnError::::de(error.err())) .map(Err) }); diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 5b656d33a1..76547aba02 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -191,9 +191,6 @@ where Self::Error, >, { - /// A unique path for the server function’s API endpoint, relative to the host, including its prefix. - const PATH: &'static str; - /// The type of the HTTP client that will send the request from the client side. /// /// For example, this might be `gloo-net` in the browser, or `reqwest` for a desktop app. @@ -227,9 +224,7 @@ where type Error: FromStr + Display; /// Returns [`Self::PATH`]. - fn url() -> &'static str { - Self::PATH - } + fn url() -> &'static str; /// Middleware that should be applied to this server function. fn middlewares( @@ -265,7 +260,7 @@ where .map(|res| (res, None)) .unwrap_or_else(|e| { ( - Self::ServerResponse::error_response(Self::PATH, &e), + Self::ServerResponse::error_response(Self::url(), &e), Some(e), ) }); @@ -275,7 +270,7 @@ where if accepts_html { // if it had an error, encode that error in the URL if let Some(err) = err { - if let Ok(url) = ServerFnUrlError::new(Self::PATH, err) + if let Ok(url) = ServerFnUrlError::new(Self::url(), err) .to_url(referer.as_deref().unwrap_or("/")) { referer = Some(url.to_string()); @@ -303,7 +298,7 @@ where async move { // create and send request on client let req = - self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?; + self.into_req(Self::url(), Self::OutputEncoding::CONTENT_TYPE)?; Self::run_on_client_with_req(req, redirect::REDIRECT_HOOK.get()) .await } @@ -489,9 +484,9 @@ pub mod axum { > + 'static, { REGISTERED_SERVER_FUNCTIONS.insert( - (T::PATH.into(), T::InputEncoding::METHOD), + (T::url().into(), T::InputEncoding::METHOD), ServerFnTraitObj::new( - T::PATH, + T::url(), T::InputEncoding::METHOD, |req| Box::pin(T::run_on_server(req)), T::middlewares, @@ -577,9 +572,9 @@ pub mod actix { > + 'static, { REGISTERED_SERVER_FUNCTIONS.insert( - (T::PATH.into(), T::InputEncoding::METHOD), + (T::url().into(), T::InputEncoding::METHOD), ServerFnTraitObj::new( - T::PATH, + T::url(), T::InputEncoding::METHOD, |req| Box::pin(T::run_on_server(req)), T::middlewares, diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 70376c77aa..44e05538bb 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -456,27 +456,8 @@ pub fn server_macro_impl( .map(|(doc, span)| quote_spanned!(*span=> #[doc = #doc])) .collect::(); - // auto-registration with inventory - let inventory = if cfg!(feature = "ssr") { - quote! { - #server_fn_path::inventory::submit! {{ - use #server_fn_path::{ServerFn, codec::Encoding}; - #server_fn_path::ServerFnTraitObj::new( - #wrapped_struct_name_turbofish::PATH, - <#wrapped_struct_name as ServerFn>::InputEncoding::METHOD, - |req| { - Box::pin(#wrapped_struct_name_turbofish::run_on_server(req)) - }, - #wrapped_struct_name_turbofish::middlewares - ) - }} - } - } else { - quote! {} - }; - // run_body in the trait implementation - let run_body = if cfg!(feature = "ssr") { + let (run_body_lint_supressor, run_body) = if cfg!(feature = "ssr") { let destructure = if let Some(wrapper) = custom_wrapper.as_ref() { quote! { let #wrapper(#struct_name #turbofish_ty_generics { #(#field_names),* }) = self; @@ -499,32 +480,35 @@ pub fn server_macro_impl( #destructure #dummy_name(#(#field_names),*).await }; - let body = if cfg!(feature = "actix") { + ( quote! { - #server_fn_path::actix::SendWrapper::new(async move { + // we need this for Actix, for the SendWrapper to count as impl Future + // but non-Actix will have a clippy warning otherwise + #[allow(clippy::manual_async_fn)] + }, + if cfg!(feature = "actix") { + quote! { + #server_fn_path::actix::SendWrapper::new(async move { + #body + }) + } + } else { + quote! { async move { #body - }) - } - } else { - quote! { async move { - #body - }} - }; - quote! { - // we need this for Actix, for the SendWrapper to count as impl Future - // but non-Actix will have a clippy warning otherwise - #[allow(clippy::manual_async_fn)] - fn run_body(self) -> impl std::future::Future + Send { - #body - } - } + }} + }, + ) } else { - quote! { - #[allow(unused_variables)] - async fn run_body(self) -> #return_ty { - unreachable!() - } - } + ( + quote! { + #[allow(unused_variables, clippy::manual_async_fn)] + }, + quote! { + async move { + unreachable!() + } + }, + ) }; // the actual function definition @@ -721,6 +705,25 @@ pub fn server_macro_impl( quote! { vec![] } }; + // auto-registration with inventory + let inventory = if cfg!(feature = "ssr") { + quote! { + #server_fn_path::inventory::submit! {{ + use #server_fn_path::{ServerFn, codec::Encoding}; + #server_fn_path::ServerFnTraitObj::new( + #path, + #input::METHOD, + |req| { + Box::pin(#wrapped_struct_name_turbofish::run_on_server(req)) + }, + #middlewares + ) + }} + } + } else { + quote! {} + }; + let quote = quote::quote! { #args_docs #docs @@ -733,8 +736,6 @@ pub fn server_macro_impl( #from_impl impl #impl_generics #server_fn_path::ServerFn for #wrapped_struct_name #where_clause { - const PATH: &'static str = #path; - type Client = #client; type ServerRequest = #req; type ServerResponse = #res; @@ -743,11 +744,18 @@ pub fn server_macro_impl( type OutputEncoding = #output; type Error = #error_ty; + fn url() -> &'static str { + #path + } + fn middlewares() -> Vec>> { #middlewares } - #run_body + #run_body_lint_supressor + fn run_body(self) -> impl std::future::Future + Send { + #run_body + } } #inventory From ad2a67f08807f5dcbad868332a3de10f7a8a5af6 Mon Sep 17 00:00:00 2001 From: Rakshith Ravi Date: Tue, 1 Oct 2024 16:47:51 +0530 Subject: [PATCH 05/11] Register generic server functions explicitly for generic types --- server_fn/src/lib.rs | 8 +++++--- server_fn_macro/src/lib.rs | 12 ++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 76547aba02..fe8ca32fd5 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -67,9 +67,11 @@ //! ad hoc HTTP API endpoint, not a magic formula. Any server function can be accessed by any HTTP //! client. You should take care to sanitize any data being returned from the function to ensure it //! does not leak data that should exist only on the server. -//! - **Server functions can’t be generic.** Because each server function creates a separate API endpoint, -//! it is difficult to monomorphize. As a result, server functions cannot be generic (for now?) If you need to use -//! a generic function, you can define a generic inner function called by multiple concrete server functions. +//! - **Generic server fns must be explicitly registered with the type.** Each server function creates +//! a separate API endpoint, which means that the URL can change depending on the generic type. As a +//! result, server functions that are generic must be explicitly registered with the +//! [`axum::register_explicit`] or [`actix::register_explicit`] function call with your generic type +//! passed into it as an argument. //! - **Arguments and return types must be serializable.** We support a variety of different encodings, //! but one way or another arguments need to be serialized to be sent to the server and deserialized //! on the server, and the return type must be serialized on the server and deserialized on the client. diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 44e05538bb..c9467c5511 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -705,8 +705,10 @@ pub fn server_macro_impl( quote! { vec![] } }; - // auto-registration with inventory - let inventory = if cfg!(feature = "ssr") { + // auto-registration with inventory only if there are no generics + let inventory = if cfg!(feature = "ssr") + && ty_generics.clone().into_token_stream().is_empty() + { quote! { #server_fn_path::inventory::submit! {{ use #server_fn_path::{ServerFn, codec::Encoding}; @@ -724,7 +726,7 @@ pub fn server_macro_impl( quote! {} }; - let quote = quote::quote! { + Ok(quote::quote! { #args_docs #docs #[derive(Debug, #derives)] @@ -763,9 +765,7 @@ pub fn server_macro_impl( #func #dummy - }; - - Ok(quote) + }) } fn type_from_ident(ident: Ident) -> Type { From 36ba3f291f2916861f459dcf53d7e5c4a33c874f Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Wed, 2 Oct 2024 19:54:38 -0400 Subject: [PATCH 06/11] fix: middlewares error --- server_fn_macro/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index c9467c5511..239efeb03b 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -718,7 +718,7 @@ pub fn server_macro_impl( |req| { Box::pin(#wrapped_struct_name_turbofish::run_on_server(req)) }, - #middlewares + #wrapped_struct_name_turbofish::middlewares ) }} } From c181b0e0015bd8f455337cfb7a06b0e40e087b80 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Wed, 2 Oct 2024 20:12:11 -0400 Subject: [PATCH 07/11] example: add full/documented example with a generic server fn --- examples/server_fns_axum/src/app.rs | 67 ++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 15485169c3..59053513e3 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -59,6 +59,7 @@ pub fn HomePage() -> impl IntoView { view! {

"Some Simple Server Functions"

+

"Custom Error Types"

@@ -75,6 +76,44 @@ pub fn HomePage() -> impl IntoView { } } +/// Server functions can be made generic, which will register multiple endpoints. +/// +/// If you use generics, you need to explicitly register the server function endpoint for each type +/// with [`server_fn::axum::register_explicit`] or [`server_fn::actix::register_explicit`] +#[component] +pub fn Generic() -> impl IntoView { + use std::fmt::Display; + + #[server] + pub async fn test_fn(input: S) -> Result + where + S: Display, + { + // insert a simulated wait + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + Ok(input.to_string()) + } + + view! { +

Generic Server Functions

+

"Server functions can be made generic, which will register multiple endpoints."

+

+ "If you use generics, you need to explicitly register the server function endpoint for each type." +

+

"Open your browser devtools to see which endpoints the function below calls."

+ + } +} + /// A server function is really just an API call to your server. But it provides a plain async /// function as a wrapper around that. This means you can call it like any other async code, just /// by spawning a task with `spawn_local`. @@ -382,7 +421,8 @@ pub fn FileUpload() -> impl IntoView {

{move || { - if upload_action.input_local().read().is_none() && upload_action.value().read().is_none() + if upload_action.input_local().read().is_none() + && upload_action.value().read().is_none() { "Upload a file.".to_string() } else if upload_action.pending().get() { @@ -782,19 +822,6 @@ pub struct WhyNotResult { modified: String, } -#[server] -pub async fn test_fn( - first: S, - second: S, -) -> Result -where - S: Display, -{ - let original = first.to_string(); - let modified = format!("{original}{second}"); - Ok(WhyNotResult { original, modified }) -} - #[server( input = Toml, output = Toml, @@ -942,13 +969,11 @@ pub fn PostcardExample() -> impl IntoView {

Using postcard encoding

"This example demonstrates using Postcard for efficient binary serialization."

+ set_input + .update(|data| { + data.age += 1; + }); + }>"Increment Age" // Display the current input data

"Input: " {move || format!("{:?}", input.get())}

From 5a4d42a154592bc39e217759b3e837b5794c3b56 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Wed, 2 Oct 2024 20:22:27 -0400 Subject: [PATCH 08/11] don't add From impl if there are generics --- server_fn_macro/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 239efeb03b..05423d564a 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -389,6 +389,7 @@ pub fn server_macro_impl( let impl_from = impl_from.map(|v| v.value).unwrap_or(true); let from_impl = (body.inputs.len() == 1 && first_field.is_some() + && body.generics.params.is_empty() && impl_from) .then(|| { let field = first_field.unwrap(); From 2d8466fdb5e22a7fb19eefccb50f84d129d095de Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Wed, 2 Oct 2024 20:22:38 -0400 Subject: [PATCH 09/11] add a generic example --- examples/server_fns_axum/src/app.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 59053513e3..8423a587b9 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -12,12 +12,12 @@ use server_fn::{ request::{browser::BrowserRequest, ClientReq, Req}, response::{browser::BrowserResponse, ClientRes, Res}, }; +use std::future::Future; #[cfg(feature = "ssr")] use std::sync::{ atomic::{AtomicU8, Ordering}, Mutex, }; -use std::{fmt::Display, future::Future}; use strum::{Display, EnumString}; use wasm_bindgen::JsCast; use web_sys::{FormData, HtmlFormElement, SubmitEvent}; @@ -103,9 +103,9 @@ pub fn Generic() -> impl IntoView {

"Open your browser devtools to see which endpoints the function below calls."