From 38645172a498d9f138b712e386a536bb4b37dab2 Mon Sep 17 00:00:00 2001 From: Sam <@> Date: Tue, 17 Dec 2024 21:05:05 -0500 Subject: [PATCH 1/6] refactor + working generics --- Cargo.lock | 1 + examples/server_fns_axum/src/app.rs | 49 + server_fn/Cargo.toml | 3 + server_fn/server_fn_macro_default/src/lib.rs | 88 ++ server_fn/src/lib.rs | 4 + server_fn_macro/Cargo.toml | 1 + server_fn_macro/src/lib.rs | 910 +++++++++++++++---- 7 files changed, 861 insertions(+), 195 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e54a4ff679..5d0d0683cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3280,6 +3280,7 @@ dependencies = [ "js-sys", "multer", "once_cell", + "paste", "pin-project-lite", "postcard", "reqwest", diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 75f5c8fe79..058503f6c1 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -72,6 +72,8 @@ pub fn HomePage() -> impl IntoView { +

"Generic Server Functions"

+ } } @@ -945,3 +947,50 @@ pub fn PostcardExample() -> impl IntoView { } } + +use std::fmt::Display; + +/// This server function is generic over S which implements Display. +/// It's registered for String and u8, which means that it will create unique routes for each specific type. +#[server] +#[register(,)] +pub async fn server_fn_to_string( + s: S, +) -> Result { + Ok(format!("{s}")) +} + +pub fn SimpleGenericServerFnComponent() -> impl IntoView { + let string_to_string = Resource::new( + move || (), + |_| async move { + server_fn_to_string(String::from( + "No wait, I'm already a string!!!", + )) + .await + }, + ); + let u8_to_string = Resource::new( + move || (), + |_| async move { server_fn_to_string(42).await }, + ); + + view! { +

Using generic function over display

+

"This example demonstrates creating a generic function that takes any type that implements display that we've registered."

+ + +

Result 1 + { + move || string_to_string.get().map(|r| r.map(|r|format!("{r}"))) + } +

+

Result 2 + { + move || u8_to_string.get().map(|r| r.map(|r|format!("{r}"))) + } +

+ +
+ } +} diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 519ebe6748..98eec6e8ca 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -75,6 +75,8 @@ reqwest = { version = "0.12.9", default-features = false, optional = true, featu ] } url = "2" pin-project-lite = "0.2.15" +# server-fn generics +paste = { version = "1.0.15", optional = true } [features] default = ["json"] @@ -112,6 +114,7 @@ rustls = ["reqwest?/rustls-tls"] reqwest = ["dep:reqwest"] ssr = ["inventory"] generic = [] +ssr_generics = ["dep:paste"] [package.metadata.docs.rs] all-features = true diff --git a/server_fn/server_fn_macro_default/src/lib.rs b/server_fn/server_fn_macro_default/src/lib.rs index 2d15987057..876be23225 100644 --- a/server_fn/server_fn_macro_default/src/lib.rs +++ b/server_fn/server_fn_macro_default/src/lib.rs @@ -68,6 +68,94 @@ use syn::__private::ToTokens; /// // etc. /// } /// ``` +/// +/// ## Generic Server Functions +/// You can make your server function generic by writing the function generically and adding a register attribute which will works like (and overrides) the endpoint attribute. +/// but for specific identities of your generic server function. +/// +/// When a generic type is not found in the inputs and is instead only found in the return type or the body the server function struct will include a `PhantomData` +/// Where T is the not found type. Or in the case of multiple not found types T,...,Tn will include them in a tuple. i.e `PhantomData<(T,...,Tn)>` +/// +/// ```rust, ignore +/// #[server] +/// #[register( +/// , +/// ="other_struct_has_specific_route" +/// )] +/// pub async fn my_generic_server_fn(input:T) -> Result<(), ServerFnError> +/// where +/// U: ThisTraitIsInAWhereClause +/// { +/// todo!() +/// } +/// +/// // expands to +/// #[derive(Deserialize, Serialize)] +/// struct MyGenericServerFn +/// where +/// // we require these traits always for generic fn input +/// T : SomeTrait + Send + Serialize + DeserializeOwned + 'static, +/// U : ThisTraitIsInAWhereClause { +/// _marker:PhantomData +/// input: T +/// } +/// +/// impl ServerFn for MyGenericServerFn { +/// // where our endpoint will be generated for us and unique to this type +/// const PATH: &'static str = "/api/...generated_endpoint..."; +/// // ... +/// } +/// +/// impl ServerFn for MyGenericServerFn { +/// const PATH: &'static str = "/api/other_struct_has_specific_route"; +/// // .. +/// } +/// ``` +/// +/// If your server function is generic over types that are not isomorphic, i.e a backend type or a database connection. You can use the `generic_fn` +/// module helper shims to create +/// the traits types and impls that the server macro will use to map the client side code onto the backend. +/// +/// You can find more details about the macros in their respective `generic_fn` module. +/// +/// ```rust,ignore +/// ssr_type_shim!(BackendType); +/// // generates +/// pub struct BackendTypePhantom; +/// #[cfg(feature="ssr")] +/// impl ServerType for BackendTypePhantom{ +/// type ServerType = BackendType; +/// } +/// ssr_trait_shim!(BackendTrait); +/// // generates +/// pub trait BackendTraitConstraint{} +/// ssr_impl_shim!(BackendType:BackendTrait); +/// // generates +/// impl BackendTypeConstraint for BackendTypePhantom{} +/// +/// // see below how we are now registered with the phantom struct and not the original struct, +/// // the server macro will "move through" the phantom struct via it's ServerType implemented above to find the server type and pass that to your server function. +/// // We do this for any specified struct, in a register attribute, with no generic parameters that ends in the word Phantom. i.e Type1Phantom, DbPhantom, Phantom, PhantomPhantom, etc. +/// #[server] +/// #[register()] +/// pub async fn generic_fn() -> Result { +/// todo!() +/// } +/// +/// // expands to +/// #[derive(Deserialize, Serialize)] +/// struct GenericFc +/// where +/// T : BackendTraitConstraint, { +/// _marker:PhantomData<(T,U)> +/// } +/// +/// impl ServerFn for GenericFn { +/// // same as above... +/// } +/// ``` +/// +/// And can be referenced in your frontend code a `T:BackendTraitConstraint`. #[proc_macro_attribute] pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { match server_macro_impl( diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 5b656d33a1..e2ffc7c747 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -114,6 +114,10 @@ pub mod request; /// Types and traits for HTTP responses. pub mod response; +/// Helpers for creating isomorphic generic code. +#[cfg(feature = "ssr_generics")] +pub mod ssr_generics; + #[cfg(feature = "actix")] #[doc(hidden)] pub use ::actix_web as actix_export; diff --git a/server_fn_macro/Cargo.toml b/server_fn_macro/Cargo.toml index 94a65b2ba6..a616d485db 100644 --- a/server_fn_macro/Cargo.toml +++ b/server_fn_macro/Cargo.toml @@ -23,6 +23,7 @@ actix = [] axum = [] generic = [] reqwest = [] +ssr_generics = [] [package.metadata.docs.rs] rustdoc-args = ["--generate-link-to-definition"] diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 864cba1204..7a4be9af1f 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -15,32 +15,10 @@ use syn::{ spanned::Spanned, *, }; +use token::Where; -/// The implementation of the `server` macro. -/// ```ignore -/// #[proc_macro_attribute] -/// pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { -/// match server_macro_impl( -/// args.into(), -/// s.into(), -/// Some(syn::parse_quote!(my_crate::exports::server_fn)), -/// ) { -/// Err(e) => e.to_compile_error().into(), -/// Ok(s) => s.to_token_stream().into(), -/// } -/// } -/// ``` -pub fn server_macro_impl( - args: TokenStream2, - body: TokenStream2, - server_fn_path: Option, - default_path: &str, - preset_req: Option, - preset_res: Option, -) -> Result { - let mut body = syn::parse::(body.into())?; - - // extract all #[middleware] attributes, removing them from signature of dummy +/// extract all #[middleware] attributes, removing them from signature of dummy +fn extract_middlewares(body: &mut ServerFnBody) -> Vec { let mut middlewares: Vec = vec![]; body.attrs.retain(|attr| { if attr.meta.path().is_ident("middleware") { @@ -54,9 +32,41 @@ pub fn server_macro_impl( true } }); + middlewares +} + +/// extract register if it exists, return an error if there's more than one +fn extract_register(body: &mut ServerFnBody) -> Result> { + let mut register: Vec = vec![]; + body.attrs.retain(|attr| { + if attr.meta.path().is_ident("register") { + if let Ok(r) = attr.parse_args() { + register.push(r); + false + } else { + true + } + } else { + true + } + }); + if register.len() > 1 { + return Err(syn::Error::new( + Span::call_site(), + "cannot use more than 1 register attribute", + )); + } + Ok(register.get(0).cloned()) +} - let fields = body - .inputs +/// construct typed fields for our server functions structure, making use of the attributes on the server function inputs. +fn fields(body: &mut ServerFnBody) -> Result> { + /* + For each function argument we figure out it's attributes, + we create a list of token streams, each item is a field the server function structure + whose type is the type from the server fn arg with the attributes from the server fn arg. + */ + body.inputs .iter_mut() .map(|f| { let typed_arg = match f { @@ -186,10 +196,12 @@ pub fn server_macro_impl( typed_arg.attrs = vec![]; Ok(quote! { #(#attrs ) * pub #typed_arg }) }) - .collect::>>()?; + .collect::>>() +} - // we need to apply the same sort of Actix SendWrapper workaround here - // that we do for the body of the function provided in the trait (see below) +/// we need to apply the same sort of Actix SendWrapper workaround here +/// that we do for the body of the function provided in the trait (see below) +fn actix_workaround(body: &mut ServerFnBody, server_fn_path: &Option) { if cfg!(feature = "actix") { let block = body.block.to_token_stream(); body.block = quote! { @@ -201,36 +213,26 @@ pub fn server_macro_impl( } }; } +} - let dummy = body.to_dummy_output(); - let dummy_name = body.to_dummy_ident(); - let args = syn::parse::(args.into())?; - - // default values for args - let ServerFnArgs { - struct_name, - prefix, - input, - input_derive, - output, - fn_path, - builtin_encoding, - req_ty, - res_ty, - client, - custom_wrapper, - impl_from, - } = args; - let prefix = prefix.unwrap_or_else(|| Literal::string(default_path)); - let fn_path = fn_path.unwrap_or_else(|| Literal::string("")); - let input_ident = match &input { +/// This gives the input encoding. +fn input_to_string(input: &Option) -> Option { + match &input { Some(Type::Path(path)) => { path.path.segments.last().map(|seg| seg.ident.to_string()) } None => Some("PostUrl".to_string()), _ => None, - }; - let input = input + } +} + +/// Construct the token stream for the input associated type of our server fn impl. +fn input_encoding_tokens( + input: Option, + builtin_encoding: bool, + server_fn_path: &Option, +) -> TokenStream2 { + input .map(|n| { if builtin_encoding { quote! { #server_fn_path::codec::#n } @@ -242,8 +244,16 @@ pub fn server_macro_impl( quote! { #server_fn_path::codec::PostUrl } - }); - let output = output + }) +} + +/// Construct token stream for our output associated type of our server fn impl. +fn output_encoding_tokens( + output: Option, + builtin_encoding: bool, + server_fn_path: &Option, +) -> TokenStream2 { + output .map(|n| { if builtin_encoding { quote! { #server_fn_path::codec::#n } @@ -255,96 +265,117 @@ pub fn server_macro_impl( quote! { #server_fn_path::codec::Json } - }); - // default to PascalCase version of function name if no struct name given - let struct_name = struct_name.unwrap_or_else(|| { + }) +} + +/// The name of the server function struct. +/// default to PascalCase version of function name if no struct name given +fn struct_name_ident(struct_name: Option, body: &ServerFnBody) -> Ident { + struct_name.unwrap_or_else(|| { let upper_camel_case_name = Converter::new() .from_case(Case::Snake) .to_case(Case::UpperCamel) .convert(body.ident.to_string()); Ident::new(&upper_camel_case_name, body.ident.span()) - }); + }) +} - // 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> } +/// If there is a custom wrapper, we wrap our struct name in it. Otherwise this will be the struct name. +fn possibly_wrap_struct_name( + struct_name: &Ident, + custom_wrapper: &Option, + ty_generics:Option<&TypeGenerics> +) -> TokenStream2 { + if let Some(wrapper) = custom_wrapper { + quote! { #wrapper<#struct_name #ty_generics> } } else { - quote! { #struct_name } - }; - let wrapped_struct_name_turbofish = - if let Some(wrapper) = custom_wrapper.as_ref() { + quote! { #struct_name #ty_generics } + } +} + + + +/// If there is a custom wrapper, we create a version with turbofish, where the argument to the turbo fish is the struct name. +/// otherwise its just the struct name. +fn possibly_wrapped_struct_name_turbofish( + struct_name: &Ident, + custom_wrapper: &Option, + ty_generics:Option<&TypeGenerics>, +) -> TokenStream2 { + if let Some(wrapper) = custom_wrapper.as_ref() { + if let Some(ty_generics) = ty_generics { + let ty_generics = ty_generics.as_turbofish(); + quote! { #wrapper::<#struct_name #ty_generics> } + } else { quote! { #wrapper::<#struct_name> } + } + } else { + if let Some(ty_generics) = ty_generics { + let ty_generics = ty_generics.as_turbofish(); + quote! { #struct_name #ty_generics } } else { - quote! { #struct_name } - }; - - // build struct for type - let fn_name = &body.ident; - let fn_name_as_str = body.ident.to_string(); - let vis = body.vis; - let attrs = body.attrs; + quote! { #struct_name #ty_generics } + } + } +} - let fn_args = body - .inputs +/// Produce the fn_args for the server function which is called from the client and the server. +fn fn_args(body: &ServerFnBody) -> Vec<&PatType> { + body.inputs .iter() .filter_map(|f| match f { FnArg::Receiver(_) => None, FnArg::Typed(t) => Some(t), }) - .collect::>(); + .collect::>() +} - let field_names = body - .inputs +/// Get just the field names of our server function structure. This is useful for deconstructing it. +fn field_names(body: &ServerFnBody) -> Vec<&Box> { + body.inputs .iter() .filter_map(|f| match f { FnArg::Receiver(_) => None, FnArg::Typed(t) => Some(&t.pat), }) - .collect::>(); + .collect::>() +} - // if there's exactly one field, impl From for the struct +/// if there's exactly one field, impl From for the struct. +/// The return type here will be the From implementation which can be added to the macro. +fn impl_from_tokens( + body: &ServerFnBody, + impl_from: Option, + struct_name: &Ident, +) -> Option { let first_field = body.inputs.iter().find_map(|f| match f { FnArg::Receiver(_) => None, FnArg::Typed(t) => Some((&t.pat, &t.ty)), }); let impl_from = impl_from.map(|v| v.value).unwrap_or(true); - let from_impl = (body.inputs.len() == 1 - && first_field.is_some() - && impl_from) - .then(|| { - 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; - #name - } - } - - impl From<#ty> for #struct_name { - fn from(#name: #ty) -> Self { - #struct_name { #name } - } + (body.inputs.len() == 1 && first_field.is_some() && impl_from).then(|| { + 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; + #name } } - }); - // check output type - let output_arrow = body.output_arrow; - let return_ty = body.return_ty; - - let output_ty = output_type(&return_ty)?; - let error_ty = err_type(&return_ty)?; - let error_ty = - error_ty.map(ToTokens::to_token_stream).unwrap_or_else(|| { - quote! { - #server_fn_path::error::NoCustomError + impl From<#ty> for #struct_name { + fn from(#name: #ty) -> Self { + #struct_name { #name } + } } - }); + } + }) +} - // build server fn path - let serde_path = server_fn_path.as_ref().map(|path| { +/// The server fn crate exports serde, create path to serde via server fn path. +fn serde_path(server_fn_path: &Option) -> Option { + server_fn_path.as_ref().map(|path| { let path = path .segments .iter() @@ -352,16 +383,11 @@ pub fn server_macro_impl( .collect::>(); let path = path.join("::"); format!("{path}::serde") - }); - let server_fn_path = server_fn_path - .map(|path| quote!(#path)) - .unwrap_or_else(|| quote! { server_fn }); - - let key_env_var = match option_env!("SERVER_FN_OVERRIDE_KEY") { - Some(_) => "SERVER_FN_OVERRIDE_KEY", - None => "CARGO_MANIFEST_DIR", - }; + }) +} +/// We add documentation to the server function structure to say what it is. (The serialized arguments of the server function) +fn args_docs_tokens(fn_name_as_str: &String) -> TokenStream2 { let link_to_server_fn = format!( "Serialized arguments for the [`{fn_name_as_str}`] server \ function.\n\n" @@ -369,16 +395,24 @@ pub fn server_macro_impl( let args_docs = quote! { #[doc = #link_to_server_fn] }; + args_docs +} - // pass through docs - let docs = body - .docs +/// Any documentation from the server function in the users code under the `#[server]` macro should appear on the client and server functions that we generate. +fn docs_tokens(body: &ServerFnBody) -> TokenStream2 { + body.docs .iter() .map(|(doc, span)| quote_spanned!(*span=> #[doc = #doc])) - .collect::(); + .collect::() +} - // auto-registration with inventory - let inventory = if cfg!(feature = "ssr") { +/// On the server we need to register the server function with inventory. Otherwise this is empty. +fn inventory_tokens( + server_fn_path: &TokenStream2, + wrapped_struct_name_turbofish: &TokenStream2, + wrapped_struct_name: &TokenStream2, +) -> TokenStream2 { + if cfg!(feature = "ssr") { quote! { #server_fn_path::inventory::submit! {{ use #server_fn_path::{ServerFn, codec::Encoding}; @@ -394,10 +428,20 @@ pub fn server_macro_impl( } } else { quote! {} - }; + } +} - // run_body in the trait implementation - let run_body = if cfg!(feature = "ssr") { +/// The ServerFn trait, has a method called run_body. We have different implementations for the server function struct for the server and the client. +/// This creates both implementations. +fn run_body_tokens( + custom_wrapper: &Option, + struct_name: &Ident, + field_names: &Vec<&Box>, + dummy_name: &Ident, + server_fn_path: &TokenStream2, + return_ty: &Type, +) -> TokenStream2 { + if cfg!(feature = "ssr") { let destructure = if let Some(wrapper) = custom_wrapper.as_ref() { quote! { let #wrapper(#struct_name { #(#field_names),* }) = self; @@ -446,40 +490,88 @@ pub fn server_macro_impl( unreachable!() } } - }; + } +} - // the actual function definition - let func = if cfg!(feature = "ssr") { +/// We generate the function that is actually called from the users code. This generates both the function called from the server, +/// and the function called from the client. +fn func_tokens( + docs: &TokenStream2, + attrs: &Vec, + vis: &Visibility, + fn_name: &Ident, + fn_args: &Vec<&PatType>, + output_arrow: &token::RArrow, + return_ty: &Type, + dummy_name: &Ident, + field_names: &Vec<&Box>, + custom_wrapper: &Option, + struct_name: &Ident, + server_fn_path: &TokenStream2, + impl_generics: Option<&ImplGenerics>, + ty_generics: Option<&TypeGenerics>, + where_clause : Option<&WhereClause>, + output_ty:&GenericArgument, + error_ty:&TokenStream2 +) -> TokenStream2 { + if cfg!(feature = "ssr") { quote! { #docs #(#attrs)* - #vis async fn #fn_name(#(#fn_args),*) #output_arrow #return_ty { + #vis async fn #fn_name #impl_generics (#(#fn_args),*) #output_arrow #return_ty #where_clause { #dummy_name(#(#field_names),*).await } } } else { + // where clause might be empty even though others are not + let where_clause = { + if ty_generics.is_some() || impl_generics.is_some() { + Some(WhereClause{where_token:Where{span:Span::call_site()},predicates:Punctuated::new()}) + } else { + where_clause.cloned() + } + }; + let where_clause = + where_clause.map(|mut where_clause|{ + // we need to extend the where clause of our restructure so that we can call .run_on_client on our data + // since our serverfn is only implemented for the types we've specified in register, we need to specify we are only calling + // run_on_client where we have implemented server fn. + where_clause.predicates.push( + parse_quote!(#struct_name #ty_generics : #server_fn_path::ServerFn ) + ); + where_clause + }); + let ty_generics = ty_generics.map(|this|this.as_turbofish()); 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 #ty_generics { #(#field_names),* }); } } else { quote! { - let data = #struct_name { #(#field_names),* }; + let data = #struct_name #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 #impl_generics (#(#fn_args),*) #output_arrow #return_ty #where_clause { use #server_fn_path::ServerFn; #restructure data.run_on_client().await } } - }; + } +} +/// Produces an additional path (to go under the derives) and derives for our server function structure, additional path could currently be the path to serde. +fn additional_path_and_derives_tokens( + input_ident: Option, + input_derive: &Option, + server_fn_path: &TokenStream2, + serde_path: &Option, +) -> (TokenStream2, TokenStream2) { enum PathInfo { Serde, Rkyv, @@ -504,7 +596,7 @@ pub fn server_macro_impl( ), _ => match input_derive { Some(derives) => { - let d = derives.elems; + let d = &derives.elems; (PathInfo::None, quote! { #d }) } None => ( @@ -522,8 +614,15 @@ pub fn server_macro_impl( PathInfo::Rkyv => quote! {}, PathInfo::None => quote! {}, }; + (addl_path, derives) +} - let client = if let Some(client) = client { +/// The code for our Client associated type on our ServerFn impl +fn client_tokens( + client: &Option, + server_fn_path: &TokenStream2, +) -> TokenStream2 { + if let Some(client) = client { client.to_token_stream() } else if cfg!(feature = "reqwest") { quote! { @@ -533,9 +632,16 @@ pub fn server_macro_impl( quote! { #server_fn_path::client::browser::BrowserClient } - }; + } +} - let req = if !cfg!(feature = "ssr") { +/// Generates the code for our Req associated type on our ServerFn impl. Generates both client and server versions, as well as framework specific code. +fn req_tokens( + server_fn_path: &TokenStream2, + req_ty: &Option, + preset_req: &Option, +) -> TokenStream2 { + if !cfg!(feature = "ssr") { quote! { #server_fn_path::request::BrowserMockReq } @@ -562,8 +668,16 @@ pub fn server_macro_impl( quote! { #server_fn_path::request::BrowserMockReq } - }; - let res = if !cfg!(feature = "ssr") { + } +} + +/// Generates the code for our Resp associated type on our ServerFn impl. Generates both server and client code, and server framework specific code. +fn resp_tokens( + server_fn_path: &TokenStream2, + res_ty: &Option, + preset_res: &Option, +) -> TokenStream2 { + if !cfg!(feature = "ssr") { quote! { #server_fn_path::response::BrowserMockRes } @@ -590,47 +704,66 @@ pub fn server_macro_impl( quote! { #server_fn_path::response::BrowserMockRes } + } +} + +/// The ServerFn impl has an associated const PATH. +fn path_tokens( + endpoint: &Literal, + server_fn_path: &TokenStream2, + prefix: &Literal, + fn_name_as_str: &String, + specified_generics:Option<&Punctuated>, +) -> TokenStream2 { + let key_env_var = match option_env!("SERVER_FN_OVERRIDE_KEY") { + Some(_) => "SERVER_FN_OVERRIDE_KEY", + None => "CARGO_MANIFEST_DIR", }; // Remove any leading slashes, even if they exist (we'll add them below) - let fn_path = Literal::string( - fn_path + let endpoint = Literal::string( + endpoint .to_string() .trim_start_matches('\"') .trim_start_matches('/') .trim_end_matches('\"'), ); - // generate path - let fn_path_starts_with_slash = fn_path.to_string().starts_with("\"/"); - let fn_path = if fn_path_starts_with_slash || fn_path.to_string() == "\"\"" - { - quote! { #fn_path } - } else { - quote! { concat!("/", #fn_path) } - }; + let endpoint_starts_with_slash = endpoint.to_string().starts_with("\"/"); + let endpoint = + if endpoint_starts_with_slash || endpoint.to_string() == "\"\"" { + quote! { #endpoint } + } else { + quote! { concat!("/", #endpoint) } + }; + let mut generics = vec![]; + for ident in specified_generics.unwrap_or(&Punctuated::new()).iter() { + generics.push(format!("{ident}")); + } + let path = quote! { - if #fn_path.is_empty() { + if #endpoint.is_empty() { #server_fn_path::const_format::concatcp!( #prefix, "/", #fn_name_as_str, #server_fn_path::xxhash_rust::const_xxh64::xxh64( - concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!()).as_bytes(), + concat!(env!(#key_env_var), ":", file!(), ":", line!(), ":", column!(), #(":", #generics)*).as_bytes(), 0 ) ) } else { #server_fn_path::const_format::concatcp!( #prefix, - #fn_path + #endpoint ) } }; + path +} - // only emit the dummy (unmodified server-only body) for the server build - let dummy = cfg!(feature = "ssr").then_some(dummy); - let middlewares = if cfg!(feature = "ssr") { +fn middlewares_tokens(middlewares: &Vec) -> TokenStream2 { + if cfg!(feature = "ssr") { quote! { vec![ #( @@ -640,43 +773,343 @@ pub fn server_macro_impl( } } else { quote! { vec![] } - }; + } +} + +fn error_ty_tokens( + return_ty: &Type, + server_fn_path: &Option, +) -> Result { + let error_ty = err_type(return_ty)?; + let error_ty = + error_ty.map(ToTokens::to_token_stream).unwrap_or_else(|| { + quote! { + #server_fn_path::error::NoCustomError + } + }); + Ok(error_ty) +} + +/// The implementation of the `server` macro. +/// ```ignore +/// #[proc_macro_attribute] +/// pub fn server(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { +/// match server_macro_impl( +/// args.into(), +/// s.into(), +/// Some(syn::parse_quote!(my_crate::exports::server_fn)), +/// ) { +/// Err(e) => e.to_compile_error().into(), +/// Ok(s) => s.to_token_stream().into(), +/// } +/// } +/// ``` +pub fn server_macro_impl( + args: TokenStream2, + body: TokenStream2, + server_fn_path: Option, + default_path: &str, + preset_req: Option, + preset_res: Option, +) -> Result { + let mut body = syn::parse::(body.into())?; + + let middlewares = extract_middlewares(&mut body); + let maybe_register = extract_register(&mut body)?; + let fields = fields(&mut body)?; + let dummy = body.to_dummy_output(); + // the dummy name is the name prefixed with __ so server_fn() -> __server_fn + let dummy_name = body.to_dummy_ident(); + let args = syn::parse::(args.into())?; - Ok(quote::quote! { - #args_docs - #docs - #[derive(Debug, #derives)] - #addl_path - pub struct #struct_name { - #(#fields),* + // default values for args + let ServerFnArgs { + struct_name, + prefix, + input, + input_derive, + output, + endpoint, + builtin_encoding, + req_ty, + res_ty, + client, + custom_wrapper, + impl_from, + } = args; + + // does nothing if no feature = actix + actix_workaround(&mut body, &server_fn_path); + + // These are used in the PATH construction for our ServerFn impl. + // They are ignored when using generics in favor of endpoints specified per registered types. + let prefix = prefix.unwrap_or_else(|| Literal::string(default_path)); + let endpoint = endpoint.unwrap_or_else(|| Literal::string("")); + + let input_ident = input_to_string(&input); + + // build struct for type + let struct_name = struct_name_ident(struct_name, &body); + let fn_name = &body.ident; + let fn_name_as_str = body.ident.to_string(); + let vis = &body.vis; + let attrs = &body.attrs; + + let fn_args = fn_args(&body); + + let field_names = field_names(&body); + + // check output type + let output_arrow = &body.output_arrow; + let return_ty = &body.return_ty; + + // build server fn path + let serde_path = serde_path(&server_fn_path); + + // turn the server fn path into a tokens, instead of using the empty string for none we'll use 'server_fn' + let server_fn_path_token = server_fn_path + .clone() + .map(|path| quote!(#path)) + .unwrap_or_else(|| quote! { server_fn }); + + let args_docs = args_docs_tokens(&fn_name_as_str); + + // pass through docs + let docs = docs_tokens(&body); + // for the server function structure + let (additional_path, derives) = additional_path_and_derives_tokens( + input_ident, + &input_derive, + &server_fn_path_token, + &serde_path, + ); + // for the associated types that are not PATH + let client = client_tokens(&client, &server_fn_path_token); + let req = req_tokens(&server_fn_path_token, &req_ty, &preset_req); + let res = resp_tokens(&server_fn_path_token, &res_ty, &preset_res); + let output_ty = output_type(return_ty)?; + let input_encoding = + input_encoding_tokens(input, builtin_encoding, &server_fn_path); + let output_encoding = + output_encoding_tokens(output, builtin_encoding, &server_fn_path); + let error_ty = error_ty_tokens(return_ty, &server_fn_path)?; + + // only emit the dummy (unmodified server-only body) for the server build + let dummy = cfg!(feature = "ssr").then_some(dummy); + if let Some(register) = maybe_register { + let (impl_generics, ty_generics, where_clause) = body.generics.split_for_impl(); + + let func = func_tokens( + &docs, + attrs, + vis, + fn_name, + &fn_args, + output_arrow, + return_ty, + &dummy_name, + &field_names, + &custom_wrapper, + &struct_name, + &server_fn_path_token, + Some(&impl_generics), + Some(&ty_generics), + where_clause, + output_ty, + &error_ty, + ); + + // for each register entry, we generate a unique implementation and inventory. + let mut implementations_and_inventories = vec![]; + for RegisterEntry { + specific_ty, + endpoint, + } in register.into_registered_entries() + { + let g : Generics = parse_quote!(< #specific_ty >); + let (_,ty_generics,_) = g.split_for_impl(); + // These will be the struct name with the specific generics for a given register arg + // i.e register() -> #struct_name + let wrapped_struct_name = + possibly_wrap_struct_name(&struct_name, &custom_wrapper,Some(&ty_generics)); + let wrapped_struct_name_turbofish = + possibly_wrapped_struct_name_turbofish( + &struct_name, + &custom_wrapper, + Some(&ty_generics), + ); + // auto-registration with inventory + let inventory = inventory_tokens( + &server_fn_path_token, + &wrapped_struct_name_turbofish, + &wrapped_struct_name, + ); + // let wrapped_struct_name include the specific_ty from RegisterEntry + + // let path = the endpoint, or else create the endpoint and stringify the specific_ty as part of the hashing. + + // run_body in the trait implementation + let run_body = run_body_tokens( + &custom_wrapper, + &struct_name, + &field_names, + &dummy_name, + &server_fn_path_token, + return_ty, + ); + // the endpoint of our path should be specified in the register attribute if at all. + let endpoint = Literal::string(&endpoint.map(|e|e.value()).unwrap_or_default()); + let path = path_tokens( + &endpoint, + &server_fn_path_token, + &prefix, + &fn_name_as_str, + Some(&specific_ty) + ); + + let middlewares = middlewares_tokens(&middlewares); + + + implementations_and_inventories.push(quote!( + impl #server_fn_path_token::ServerFn for #wrapped_struct_name { + const PATH: &'static str = #path; + + type Client = #client; + type ServerRequest = #req; + type ServerResponse = #res; + type Output = #output_ty; + type InputEncoding = #input_encoding; + type OutputEncoding = #output_encoding; + type Error = #error_ty; + + fn middlewares() -> Vec>> { + #middlewares + } + + #run_body + } + + #inventory + )) } + Ok(quote!( + #args_docs + #docs + #[derive(Debug, #derives)] + #additional_path + pub struct #struct_name #impl_generics #where_clause { + #(#fields),* + } - #from_impl + #(#implementations_and_inventories)* - impl #server_fn_path::ServerFn for #wrapped_struct_name { - const PATH: &'static str = #path; + #func - type Client = #client; - type ServerRequest = #req; - type ServerResponse = #res; - type Output = #output_ty; - type InputEncoding = #input; - type OutputEncoding = #output; - type Error = #error_ty; + #dummy + + )) + } else { + // struct name, wrapped in any custom-encoding newtype wrapper (if it exists, otherwise it's just the struct name) + let wrapped_struct_name = + possibly_wrap_struct_name(&struct_name, &custom_wrapper,None); + let wrapped_struct_name_turbofish = + possibly_wrapped_struct_name_turbofish( + &struct_name, + &custom_wrapper, + None, + ); + + + + let from_impl = impl_from_tokens(&body, impl_from, &struct_name); + + + // auto-registration with inventory + let inventory = inventory_tokens( + &server_fn_path_token, + &wrapped_struct_name_turbofish, + &wrapped_struct_name, + ); + + // run_body in the trait implementation + let run_body = run_body_tokens( + &custom_wrapper, + &struct_name, + &field_names, + &dummy_name, + &server_fn_path_token, + return_ty, + ); - fn middlewares() -> Vec>> { - #middlewares + // the actual function definition + let func = func_tokens( + &docs, + attrs, + vis, + fn_name, + &fn_args, + output_arrow, + return_ty, + &dummy_name, + &field_names, + &custom_wrapper, + &struct_name, + &server_fn_path_token, + None,None,None, + output_ty, + &error_ty + ); + + + + let path = path_tokens( + &endpoint, + &server_fn_path_token, + &prefix, + &fn_name_as_str, + None + ); + + + + let middlewares = middlewares_tokens(&middlewares); + + Ok(quote::quote! { + #args_docs + #docs + #[derive(Debug, #derives)] + #additional_path + pub struct #struct_name { + #(#fields),* } - #run_body - } + #from_impl - #inventory + impl #server_fn_path_token::ServerFn for #wrapped_struct_name { + const PATH: &'static str = #path; - #func + type Client = #client; + type ServerRequest = #req; + type ServerResponse = #res; + type Output = #output_ty; + type InputEncoding = #input_encoding; + type OutputEncoding = #output_encoding; + type Error = #error_ty; - #dummy - }) + fn middlewares() -> Vec>> { + #middlewares + } + + #run_body + } + + #inventory + + #func + + #dummy + }) + } } fn type_from_ident(ident: Ident) -> Type { @@ -715,6 +1148,90 @@ impl Parse for Middleware { } } +#[derive(Debug, Clone)] +struct Register { + /// Matches something like: ( = "some_string", , ...) + type_value_pairs: + Punctuated<(Punctuated, Option), Token![,]>, +} +struct RegisterEntry { + specific_ty: Punctuated, + endpoint: Option, +} +impl Register { + fn into_registered_entries(self) -> Vec { + self.type_value_pairs + .into_iter() + .map(|(specific_ty, endpoint)| RegisterEntry { + specific_ty, + endpoint, + }) + .collect() + } + /// If any specific registered type in the register attribute ends with the word Phantom and we have enabled ssr_generics then + /// we have backend generics + fn has_backend_generics(&self) -> bool { + self.type_value_pairs.iter().any(|(specific_ty, _)| { + specific_ty + .iter() + .any(|ty| ty.to_string().ends_with("Phantom")) + }) && cfg!(feature = "ssr_generics") + } +} +impl Parse for Register { + fn parse(input: ParseStream) -> Result { + let mut pairs = Punctuated::new(); + + while input.peek(Token![<]) { + // Parse the angle bracketed ident list + input.parse::()?; + let idents = + Punctuated::::parse_separated_nonempty( + input, + )?; + input.parse::]>()?; + + // Optionally parse `= "..."` if present + let maybe_value = if input.peek(Token![=]) { + input.parse::()?; + let lit = input.parse::()?; + Some(lit) + } else { + None + }; + + pairs.push((idents, maybe_value)); + + // If there's a comma, consume it and parse the next entry + if input.peek(Token![,]) { + input.parse::()?; + } else { + break; + } + } + + let register = Register { + type_value_pairs: pairs, + }; + + // ensure all brackets have the same len + let expected_len = register + .type_value_pairs + .first() + .map(|(p, _)| p.len()) + .unwrap_or(0); + for (p, _) in ®ister.type_value_pairs { + if p.len() != expected_len { + return Err(syn::Error::new( + p.span(), + "All bracketed lists must have the same length in register", + )); + } + } + + Ok(register) + } +} fn output_type(return_ty: &Type) -> Result<&GenericArgument> { if let syn::Type::Path(pat) = &return_ty { if pat.path.segments[0].ident == "Result" { @@ -784,7 +1301,7 @@ struct ServerFnArgs { input: Option, input_derive: Option, output: Option, - fn_path: Option, + endpoint: Option, req_ty: Option, res_ty: Option, client: Option, @@ -799,7 +1316,7 @@ impl Parse for ServerFnArgs { let mut struct_name: Option = None; let mut prefix: Option = None; let mut encoding: Option = None; - let mut fn_path: Option = None; + let mut endpoint: Option = None; // new arguments: can only be keyed by name let mut input: Option = None; @@ -850,13 +1367,13 @@ impl Parse for ServerFnArgs { } encoding = Some(stream.parse()?); } else if key == "endpoint" { - if fn_path.is_some() { + if endpoint.is_some() { return Err(syn::Error::new( key.span(), "keyword argument repeated: `endpoint`", )); } - fn_path = Some(stream.parse()?); + endpoint = Some(stream.parse()?); } else if key == "input" { if encoding.is_some() { return Err(syn::Error::new( @@ -967,7 +1484,7 @@ impl Parse for ServerFnArgs { 1 => return Err(lookahead.error()), 2 => prefix = Some(value), 3 => encoding = Some(value), - 4 => fn_path = Some(value), + 4 => endpoint = Some(value), _ => { return Err(syn::Error::new( value.span(), @@ -1023,7 +1540,7 @@ impl Parse for ServerFnArgs { input, input_derive, output, - fn_path, + endpoint, builtin_encoding, req_ty, res_ty, @@ -1121,6 +1638,7 @@ impl ServerFnBody { Ident::new(&format!("__{}", self.ident), self.ident.span()) } + /// The dummy is the original function that was annotated with `#[server]`. fn to_dummy_output(&self) -> TokenStream2 { let ident = self.to_dummy_ident(); let Self { @@ -1135,10 +1653,12 @@ impl ServerFnBody { block, .. } = &self; + // will all be empty if no generics... + let (impl_generics,_,where_clause) = generics.split_for_impl(); quote! { #[doc(hidden)] #(#attrs)* - #vis #async_token #fn_token #ident #generics ( #inputs ) #output_arrow #return_ty + #vis #async_token #fn_token #ident #impl_generics ( #inputs ) #output_arrow #return_ty #where_clause #block } } From ccc51f14fbed756b828c197eb359c4d8bc36d993 Mon Sep 17 00:00:00 2001 From: Sam <@> Date: Tue, 17 Dec 2024 21:06:45 -0500 Subject: [PATCH 2/6] working on generic over backend --- server_fn/src/ssr_generics.rs | 142 ++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 server_fn/src/ssr_generics.rs diff --git a/server_fn/src/ssr_generics.rs b/server_fn/src/ssr_generics.rs new file mode 100644 index 0000000000..375e493917 --- /dev/null +++ b/server_fn/src/ssr_generics.rs @@ -0,0 +1,142 @@ +//! +//! This module contains macros that generate code that can be used when turning server only generic server functions into isomorphic generic functions. +//! The way it works is that we produce phantom types and empty trait constraints that shadow server only types and traits and within the server function macro +//! it connects the shims. +//! +//! for example +//! +//! ```rust, ignore +//! +//! // Supose we have two server only types `BackendType` and `MockBackendTrait` +//! // We can generate a "phantom" type for each of our server only types. +//! ssr_type_shim!(BackendType,MockBackendTrait); +//! +//! // Suppose we had a server only trait `BackendTrait` +//! // We can generate empty "constraint" traits for our server only traits; +//! ssr_trait_shim!(BackendTrait); +//! +//! // And suppose that our backend types implemented our backend trait. +//! // We can implement our constraint traits over our phantom types. +//! ssr_impl_shim!(BackendType:BackendTrait,MockBackendTrait:BackendTrait); +//! +//! // Now we're able to write a component for our frontend that is generic over it's backend implementation. +//! #[component] +//! pub fn MyComponent() -> impl IntoView { +//! Suspense::new(async move { +//! get_data::().await.unwrap().into_view() +//! }) +//! } +//! +//! // We register every different type configurations that we want access to. +//! // This is because each specific monomorphized function needs a seperate route. +//! #[server] +//! #[register(,)] +//! pub async fn get_data() -> Result { +//! // Suppose there existed a BackendTrait which implemented this method. +//! Ok(T::my_data().await?) +//! } +//! +//! // Now we can create two frontend functions that each elicit different backend behavior based on their type. +//! #[component] +//! pub fn MyComponentParent() -> impl IntoView { +//! view!{ +//! > +//! > +//! } +//! } +//! ``` + +#[doc(hidden)] +pub trait ServerType { + type ServerType; +} +/// Generates a new struct $type_namePhantom for a list of identifiers. +/// +/// ```rust,ignore +/// ssr_type_shim!(SpecificType); +/// +/// fn main() { +/// let _ = SpecificTypePhantom{}; +/// } +/// ``` +/// +/// It also implements a hidden trait ServerType under an ssr feature flag whose associated type is the original server only type. +macro_rules! ssr_type_shim{ + ($($type_name:ident),*) => { + $( + paste::paste!{ + /// An isomorphic marker type for $type_name + pub struct [<$type_name Phantom>]; + } + )* + $( + #[cfg(feature="ssr")] + paste::paste! { impl ServerType for [<$type_name Phantom>] { + type ServerType = $type_name; + } + } + )* + } +} + +/// Generates new empty traits $trait_nameConstraint for a list of identifiers. +/// +/// /// ```rust,ignore +/// ssr_type_shim!(SpecificTrait); +/// +/// // Will generate code +/// // pub trait SpecificTraitConstraint{} +/// ``` +/// +macro_rules! ssr_trait_shim{ + ($($trait_name:ident),*) => { + $( + paste::paste! { + /// An empty isomorphic trait to mirror $trait_name + pub trait [<$trait_name Constraint>] {} + } + )* + } +} + +/// Takes type names and trait names for the traits they need implemented and implements the "constraint" traits for the "phantom" versions. +/// +/// ```rust,ignore +/// // uses traditional + syntax for additonal traits past 1 like in normal trait bounds. +/// ssr_impl_shim!(BackendType:BackendTrait, BackendType2:BackendTrait + BackendTrait2); +/// ``` +macro_rules! ssr_impl_shim{ + ($($type_name:ident : $trait_name:ident $(+ $trait_name_tail:ident)*),*) => { + $( + paste:: paste! { impl [<$trait_name Constraint>] for [<$type_name Phantom>] {} } + $( + paste:: paste! { impl [<$trait_name_tail Constraint>] for [<$type_name Phantom>] {} } + )* + )* + } +} + +#[doc(hidden)] +#[cfg(test)] +pub mod tests { + use super::*; + + #[test] + fn server_fn_shims_generate_code_correctly() { + pub struct BackendType; + pub trait BackendTrait {} + impl BackendTrait for BackendType {} + ssr_type_shim!(BackendType); + ssr_trait_shim!(BackendTrait); + ssr_impl_shim!(BackendType:BackendTrait); + + pub fn generic_fn() + where + ::ServerType: BackendTrait, + { + } + generic_fn::(); + // If this compiles it passes. + assert!(true); + } +} From 62a2f2f97ab71742dcd6a4ac4f94c1a0a27c67aa Mon Sep 17 00:00:00 2001 From: Sam <@> Date: Sat, 21 Dec 2024 14:35:39 -0500 Subject: [PATCH 3/6] ServerFn Generics finished. --- examples/server_fns_axum/Cargo.toml | 1 + examples/server_fns_axum/src/app.rs | 203 ++++++++- server_fn/src/lib.rs | 4 +- server_fn/src/ssr_generics.rs | 14 +- server_fn_macro/src/lib.rs | 636 +++++++++++++++++++++------- 5 files changed, 681 insertions(+), 177 deletions(-) diff --git a/examples/server_fns_axum/Cargo.toml b/examples/server_fns_axum/Cargo.toml index 1a83c67c9c..088b64edd0 100644 --- a/examples/server_fns_axum/Cargo.toml +++ b/examples/server_fns_axum/Cargo.toml @@ -17,6 +17,7 @@ server_fn = { path = "../../server_fn", features = [ "rkyv", "multipart", "postcard", + "ssr_generics", ] } log = "0.4.22" simple_logger = "5.0" diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 058503f6c1..1310870bda 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -57,7 +57,8 @@ pub fn App() -> impl IntoView { #[component] pub fn HomePage() -> impl IntoView { view! { -

"Some Simple Server Functions"

+ +

"Some Simple Server Functions"

@@ -74,6 +75,11 @@ pub fn HomePage() -> impl IntoView {

"Generic Server Functions"

+ + + + + } } @@ -957,40 +963,201 @@ use std::fmt::Display; pub async fn server_fn_to_string( s: S, ) -> Result { + println!("Type {} got arg {s}", std::any::type_name::()); Ok(format!("{s}")) } +#[component] pub fn SimpleGenericServerFnComponent() -> impl IntoView { let string_to_string = Resource::new( - move || (), - |_| async move { - server_fn_to_string(String::from( - "No wait, I'm already a string!!!", - )) - .await - }, + || (), + |_| server_fn_to_string(String::from("I'm a String.")), ); let u8_to_string = Resource::new( move || (), - |_| async move { server_fn_to_string(42).await }, + |_| async move { server_fn_to_string::(42).await }, ); - view! {

Using generic function over display

"This example demonstrates creating a generic function that takes any type that implements display that we've registered."

- -

Result 1 + + + +

Result 1

+

+ { + move || string_to_string.get() + } +

+

Result 2

+

+ { + move || u8_to_string.get() + } +

+ + + + + } +} + +pub trait SomeTrait { + fn some_method() -> String; +} + +pub struct AStruct; +pub struct AStruct2; +impl SomeTrait for AStruct { + fn some_method() -> String { + String::from("Hello world...") + } +} +impl SomeTrait for AStruct2 { + fn some_method() -> String { + String::from("HELLO WORLD") + } +} + +#[server] +#[register(,)] +pub async fn server_hello_world_generic( +) -> Result { + Ok(S::some_method()) +} + +#[component] +pub fn GenericHelloWorld() -> impl IntoView { + let s = RwSignal::new(String::new()); + Effect::new(move |_| { + spawn_local(async move { + match server_hello_world_generic::().await { + Ok(hello) => s.set(hello), + Err(err) => leptos::logging::log!("{err:?}"), + } + }) + }); + let s2 = RwSignal::new(String::new()); + Effect::new(move |_| { + spawn_local(async move { + match server_hello_world_generic::().await { + Ok(hello) => s2.set(hello), + Err(err) => leptos::logging::log!("{err:?}"), + } + }) + }); + view! { +

Using generic functions without generic inputs

+

"This example demonstrates creating a generic server function that doesn't take any generic input."

+

{"Results"}

+

{ move || format!("With generic specified to {} we get {} from the server", std::any::type_name::(), s.get())}

+

{ move || format!("With generic specified to {} we get {} from the server", std::any::type_name::(), s2.get())}

+ + } +} + +#[derive(Clone)] +struct SomeDefault; +impl SomeTrait for SomeDefault { + fn some_method() -> String { + String::from("just a default hello world...") + } +} + +#[server] +#[register()] +pub async fn server_hello_world_generic_with_default< + S: SomeTrait = SomeDefault, +>() -> Result { + Ok(S::some_method()) +} + +#[component] +pub fn GenericHelloWorldWithDefaults() -> impl IntoView { + let action = ServerAction::::new(); + Effect::new(move |_| { + action.dispatch(ServerHelloWorldGenericWithDefault { + _marker: std::marker::PhantomData, + }); + }); + + view! { +

Using generic functions without generic inputs but a specified default type

+

"This example demonstrates creating a generic server function that doesn't take any generic input and has a default generic type."

+

{"Results"}

+

{ move || action.value_local().read_only().get().map(|s|s.map(|s|format!("With default generic we get {s} from the server", )))}

+ } +} + +#[cfg(feature = "ssr")] +pub struct ServerOnlyStruct; +#[cfg(feature = "ssr")] +pub struct SsrOnlyStructButDifferent; +#[cfg(feature = "ssr")] +pub trait ServerOnlyTrait { + fn some_method() -> String { + String::from("I'm a backend!") + } +} + +#[cfg(feature = "ssr")] +impl ServerOnlyTrait for ServerOnlyStruct {} +#[cfg(feature = "ssr")] +impl ServerOnlyTrait for SsrOnlyStructButDifferent { + fn some_method() -> String { + String::from("I'm a different backend!") + } +} +server_fn::ssr_type_shim! {ServerOnlyStruct, SsrOnlyStructButDifferent} +server_fn::ssr_trait_shim! {ServerOnlyTrait} +server_fn::ssr_impl_shim! {ServerOnlyStruct:ServerOnlyTrait,SsrOnlyStructButDifferent:ServerOnlyTrait} + +#[server] +#[register(, + +)] +pub async fn generic_server_with_ssr_only_types( +) -> Result { + Ok(T::some_method()) +} + +#[component] +pub fn GenericSsrOnlyTypes() -> impl IntoView { + let s = RwSignal::new(String::new()); + // spawn on the client + Effect::new(move |_| { + spawn_local(async move { + match generic_server_with_ssr_only_types::( + ) + .await { - move || string_to_string.get().map(|r| r.map(|r|format!("{r}"))) + Ok(hello) => s.set(hello), + Err(err) => leptos::logging::log!("{err:?}"), } -

-

Result 2 + }); + }); + + let s2 = RwSignal::new(String::new()); + // spawn on the client + Effect::new(move |_| { + spawn_local(async move { + match generic_server_with_ssr_only_types::< + SsrOnlyStructButDifferentPhantom, + >() + .await { - move || u8_to_string.get().map(|r| r.map(|r|format!("{r}"))) + Ok(hello) => s2.set(hello), + Err(err) => leptos::logging::log!("{err:?}"), } -

+ }); + }); + view! { +

Using generic functions with a type that only exists on the server.

+

"This example demonstrates how to make use of the helper macros and phantom types to make your backend generic and specifiable from your frontend."

+

{"Results"}

+

{ move || format!("With backend 1 we get {} from the server", s.get())}

+

{ move || format!("With backend 2 we get {} from the server", s2.get())}

-
} } diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index e2ffc7c747..b139015743 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -117,7 +117,6 @@ pub mod response; /// Helpers for creating isomorphic generic code. #[cfg(feature = "ssr_generics")] pub mod ssr_generics; - #[cfg(feature = "actix")] #[doc(hidden)] pub use ::actix_web as actix_export; @@ -130,6 +129,9 @@ pub use ::bytes as bytes_export; #[cfg(feature = "generic")] #[doc(hidden)] pub use ::http as http_export; +#[cfg(feature = "ssr_generics")] +#[doc(hidden)] +pub use ::paste as paste_export; use client::Client; use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; #[doc(hidden)] diff --git a/server_fn/src/ssr_generics.rs b/server_fn/src/ssr_generics.rs index 375e493917..d521d176f8 100644 --- a/server_fn/src/ssr_generics.rs +++ b/server_fn/src/ssr_generics.rs @@ -61,17 +61,18 @@ pub trait ServerType { /// ``` /// /// It also implements a hidden trait ServerType under an ssr feature flag whose associated type is the original server only type. +#[macro_export] macro_rules! ssr_type_shim{ ($($type_name:ident),*) => { $( - paste::paste!{ + $crate::paste_export::paste!{ /// An isomorphic marker type for $type_name pub struct [<$type_name Phantom>]; } )* $( #[cfg(feature="ssr")] - paste::paste! { impl ServerType for [<$type_name Phantom>] { + $crate::paste_export::paste! { impl $crate::ssr_generics::ServerType for [<$type_name Phantom>] { type ServerType = $type_name; } } @@ -87,11 +88,11 @@ macro_rules! ssr_type_shim{ /// // Will generate code /// // pub trait SpecificTraitConstraint{} /// ``` -/// +#[macro_export] macro_rules! ssr_trait_shim{ ($($trait_name:ident),*) => { $( - paste::paste! { + $crate::paste_export::paste! { /// An empty isomorphic trait to mirror $trait_name pub trait [<$trait_name Constraint>] {} } @@ -105,12 +106,13 @@ macro_rules! ssr_trait_shim{ /// // uses traditional + syntax for additonal traits past 1 like in normal trait bounds. /// ssr_impl_shim!(BackendType:BackendTrait, BackendType2:BackendTrait + BackendTrait2); /// ``` +#[macro_export] macro_rules! ssr_impl_shim{ ($($type_name:ident : $trait_name:ident $(+ $trait_name_tail:ident)*),*) => { $( - paste:: paste! { impl [<$trait_name Constraint>] for [<$type_name Phantom>] {} } + $crate::paste_export:: paste! { impl [<$trait_name Constraint>] for [<$type_name Phantom>] {} } $( - paste:: paste! { impl [<$trait_name_tail Constraint>] for [<$type_name Phantom>] {} } + ::server_fn::paste_export:: paste! { impl [<$trait_name_tail Constraint>] for [<$type_name Phantom>] {} } )* )* } diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 7a4be9af1f..50fc1d82fc 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -59,13 +59,44 @@ fn extract_register(body: &mut ServerFnBody) -> Result> { Ok(register.get(0).cloned()) } +/// Takes body, and returns a list of field types to compare to. +fn input_types(body: &ServerFnBody) -> Vec { + body.inputs + .iter() + .filter_map(|input| { + if let FnArg::Typed(pat) = input { + let ty = pat.ty.clone(); + let ident: Ident = parse_quote!(#ty); + Some(ident) + } else { + None + } + }) + .collect::>() +} + +fn extend_fields_with_phantom_types( + fields: Vec, + phantom_types: Vec, +) -> Vec { + if !phantom_types.is_empty() { + let mut fields = fields; + let q = quote! { + #[serde(skip)] + #[allow(unused_parens)] + pub _marker: ::std::marker::PhantomData<(#(#phantom_types),*)> + + }; + // panic!("{:?}",phantom_types); + fields.push(q); + fields + } else { + fields + } +} + /// construct typed fields for our server functions structure, making use of the attributes on the server function inputs. fn fields(body: &mut ServerFnBody) -> Result> { - /* - For each function argument we figure out it's attributes, - we create a list of token streams, each item is a field the server function structure - whose type is the type from the server fn arg with the attributes from the server fn arg. - */ body.inputs .iter_mut() .map(|f| { @@ -284,7 +315,7 @@ fn struct_name_ident(struct_name: Option, body: &ServerFnBody) -> Ident { fn possibly_wrap_struct_name( struct_name: &Ident, custom_wrapper: &Option, - ty_generics:Option<&TypeGenerics> + ty_generics: Option<&TypeGenerics>, ) -> TokenStream2 { if let Some(wrapper) = custom_wrapper { quote! { #wrapper<#struct_name #ty_generics> } @@ -293,14 +324,12 @@ fn possibly_wrap_struct_name( } } - - /// If there is a custom wrapper, we create a version with turbofish, where the argument to the turbo fish is the struct name. /// otherwise its just the struct name. fn possibly_wrapped_struct_name_turbofish( struct_name: &Ident, custom_wrapper: &Option, - ty_generics:Option<&TypeGenerics>, + ty_generics: Option<&TypeGenerics>, ) -> TokenStream2 { if let Some(wrapper) = custom_wrapper.as_ref() { if let Some(ty_generics) = ty_generics { @@ -440,18 +469,44 @@ fn run_body_tokens( dummy_name: &Ident, server_fn_path: &TokenStream2, return_ty: &Type, + has_marker: bool, + specific_ty_phantom_suffix_removed: Option<&TypeGenerics>, ) -> TokenStream2 { if cfg!(feature = "ssr") { + let marker: Box = Box::new(parse_quote!(_marker)); + // We are taking references in the function, I don't want to propagate type changes so I wrote this instead. + let possibly_extend_field_names_with_marker = + |field_names: &Vec<&Box>| -> Vec> { + if has_marker { + let mut field_names = field_names.clone(); + field_names.push(&marker); + field_names + .into_iter() + .map(|t| t.clone()) + .collect::>() + } else { + field_names + .clone() + .into_iter() + .map(|t| t.clone()) + .collect::>() + } + }; let destructure = if let Some(wrapper) = custom_wrapper.as_ref() { + let field_names = + possibly_extend_field_names_with_marker(field_names); quote! { let #wrapper(#struct_name { #(#field_names),* }) = self; } } else { + let field_names = + possibly_extend_field_names_with_marker(field_names); quote! { let #struct_name { #(#field_names),* } = self; } }; - + let specific_ty = + specific_ty_phantom_suffix_removed.map(|ty| ty.as_turbofish()); // using the impl Future syntax here is thanks to Actix // // if we use Actix types inside the function, here, it becomes !Send @@ -462,7 +517,7 @@ fn run_body_tokens( // however, SendWrapper> impls Future let body = quote! { #destructure - #dummy_name(#(#field_names),*).await + #dummy_name #specific_ty (#(#field_names),*).await }; let body = if cfg!(feature = "actix") { quote! { @@ -493,6 +548,83 @@ fn run_body_tokens( } } +/// Our function that we use on the server (and wraps the dummy function), if it has types that take Traits that end with Constraint +/// Then we map those into their ServerTypes and pass them to the dummy function. +fn extend_generics_phantom_suffix_with_server_type_trait( + impl_generics: Option<&ImplGenerics>, + where_clause: Option<&WhereClause>, +) -> (Generics, TokenStream2) { + let signature: Signature = + parse_quote!(fn dummy #impl_generics () #where_clause); + let mut g: Generics = signature.generics; + let server_type: Type = parse_quote!(::server_fn::ssr_generics::ServerType); + let mut server_type_predicates = Vec::new(); + let mut type_has_trait_constraints = std::collections::HashSet::new(); + // Go through each type parameters in the generics for our original server function + for ty in g.type_params_mut() { + let mut server_type_constraints: Punctuated = + Punctuated::new(); + let mut propagate_flag = false; + // Type is a ssr only type if it implements a TraitConstraint for an ssr only trait, + // strip the constraint suffix and propagate the bounds to our ServerType where predicate + for type_bound in ty.bounds.iter() { + if let TypeParamBound::Trait(trait_bound) = type_bound { + if let Some(last) = trait_bound.path.segments.last() { + let ident = last.ident.clone(); + let ident_str = ident.to_string(); + let ident = if ident_str.ends_with("Constraint") + && ident_str.len() > "Constraint".len() + { + type_has_trait_constraints.insert(ty.ident.clone()); + propagate_flag = true; + Ident::new( + &ident_str[0..ident_str.len() - "Constraint".len()], + Span::call_site(), + ) + } else { + ident + }; + server_type_constraints.push(ident); + } + } + } + let ident = &ty.ident; + if propagate_flag { + // Add a where predicate to the public ssr server function that requires that TPhantom implement ServerType. + ty.bounds.push(parse_quote!(#server_type)); + // Now add the where predicate we've constructed for the server type. + let predicate: WherePredicate = parse_quote!(< #ident as #server_type > :: ServerType : #server_type_constraints); + + server_type_predicates.push(predicate); + } + } + let where_clause = g.make_where_clause(); + for predicate in server_type_predicates { + where_clause.predicates.push(predicate); + } + // No we have the generics for the ssr server function, we need to construct the canonocalized generics for our dummy function that we call inside of the server function, + let generics_for_ssr_server_function = g; + let signature: Signature = parse_quote!(fn dummy #impl_generics ()); + // Our dummy function doesn't take TPhantom, it instead takes ::ServerType. + let mut g: Generics = signature.generics; + let mut types: Punctuated = Punctuated::new(); + for ty in g.type_params_mut() { + let ident = &ty.ident; + // if the original server function is accepting a T : TraitConstraint + if type_has_trait_constraints.contains(ident) { + types.push(quote!(< #ident as #server_type> :: ServerType)); + } else { + types.push(quote!(#ident)) + } + } + // we return the generics for the outer function and the turbo fish types for the dummy function + let dummy_fish = if types.is_empty() { + quote!() + } else { + quote!(:: < #types >) + }; + (generics_for_ssr_server_function, dummy_fish) +} /// We generate the function that is actually called from the users code. This generates both the function called from the server, /// and the function called from the client. fn func_tokens( @@ -509,30 +641,48 @@ fn func_tokens( struct_name: &Ident, server_fn_path: &TokenStream2, impl_generics: Option<&ImplGenerics>, + impl_generics_with_trait_constraints_and_phantom_suffix: Option< + &ImplGenerics, + >, ty_generics: Option<&TypeGenerics>, - where_clause : Option<&WhereClause>, - output_ty:&GenericArgument, - error_ty:&TokenStream2 + where_clause: Option<&WhereClause>, + output_ty: &GenericArgument, + error_ty: &TokenStream2, + has_marker: bool, ) -> TokenStream2 { if cfg!(feature = "ssr") { + let (outer_generics, dummy_types) = + extend_generics_phantom_suffix_with_server_type_trait( + impl_generics_with_trait_constraints_and_phantom_suffix, + where_clause, + ); + + let (impl_generics, _, where_clause) = outer_generics.split_for_impl(); quote! { #docs #(#attrs)* #vis async fn #fn_name #impl_generics (#(#fn_args),*) #output_arrow #return_ty #where_clause { - #dummy_name(#(#field_names),*).await + #dummy_name #dummy_types (#(#field_names),*).await } } } else { // where clause might be empty even though others are not let where_clause = { - if ty_generics.is_some() || impl_generics.is_some() { - Some(WhereClause{where_token:Where{span:Span::call_site()},predicates:Punctuated::new()}) + if ty_generics.is_some() + || impl_generics_with_trait_constraints_and_phantom_suffix + .is_some() + { + Some(WhereClause { + where_token: Where { + span: Span::call_site(), + }, + predicates: Punctuated::new(), + }) } else { where_clause.cloned() } }; - let where_clause = - where_clause.map(|mut where_clause|{ + let where_clause = where_clause.map(|mut where_clause|{ // we need to extend the where clause of our restructure so that we can call .run_on_client on our data // since our serverfn is only implemented for the types we've specified in register, we need to specify we are only calling // run_on_client where we have implemented server fn. @@ -541,7 +691,19 @@ fn func_tokens( ); where_clause }); - let ty_generics = ty_generics.map(|this|this.as_turbofish()); + let ty_generics = ty_generics.map(|this| this.as_turbofish()); + let mut field_names = field_names.clone(); + let marker = Box::new(parse_quote!(_marker)); + if has_marker { + field_names.push(&marker); + } + let make_marker = if has_marker { + let turbo_fish = + ty_generics.clone().expect("has_marker iff ty_generics"); + quote!(let _marker = ::std::marker::PhantomData #turbo_fish ;) + } else { + quote!() + }; let restructure = if let Some(custom_wrapper) = custom_wrapper.as_ref() { quote! { @@ -556,8 +718,9 @@ fn func_tokens( #docs #(#attrs)* #[allow(unused_variables)] - #vis async fn #fn_name #impl_generics (#(#fn_args),*) #output_arrow #return_ty #where_clause { + #vis async fn #fn_name #impl_generics_with_trait_constraints_and_phantom_suffix (#(#fn_args),*) #output_arrow #return_ty #where_clause { use #server_fn_path::ServerFn; + #make_marker #restructure data.run_on_client().await } @@ -713,7 +876,7 @@ fn path_tokens( server_fn_path: &TokenStream2, prefix: &Literal, fn_name_as_str: &String, - specified_generics:Option<&Punctuated>, + specified_generics: Option<&Punctuated>, ) -> TokenStream2 { let key_env_var = match option_env!("SERVER_FN_OVERRIDE_KEY") { Some(_) => "SERVER_FN_OVERRIDE_KEY", @@ -736,11 +899,11 @@ fn path_tokens( } else { quote! { concat!("/", #endpoint) } }; - let mut generics = vec![]; - for ident in specified_generics.unwrap_or(&Punctuated::new()).iter() { - generics.push(format!("{ident}")); - } - + let mut generics = vec![]; + for ident in specified_generics.unwrap_or(&Punctuated::new()).iter() { + generics.push(format!("{ident}")); + } + let path = quote! { if #endpoint.is_empty() { #server_fn_path::const_format::concatcp!( @@ -762,6 +925,7 @@ fn path_tokens( path } +/// Convert our middlewares list into a token stream for interpolation. fn middlewares_tokens(middlewares: &Vec) -> TokenStream2 { if cfg!(feature = "ssr") { quote! { @@ -817,6 +981,7 @@ pub fn server_macro_impl( let middlewares = extract_middlewares(&mut body); let maybe_register = extract_register(&mut body)?; let fields = fields(&mut body)?; + let dummy = body.to_dummy_output(); // the dummy name is the name prefixed with __ so server_fn() -> __server_fn let dummy_name = body.to_dummy_ident(); @@ -847,7 +1012,7 @@ pub fn server_macro_impl( let endpoint = endpoint.unwrap_or_else(|| Literal::string("")); let input_ident = input_to_string(&input); - + // build struct for type let struct_name = struct_name_ident(struct_name, &body); let fn_name = &body.ident; @@ -876,104 +1041,128 @@ pub fn server_macro_impl( // pass through docs let docs = docs_tokens(&body); - // for the server function structure - let (additional_path, derives) = additional_path_and_derives_tokens( - input_ident, - &input_derive, - &server_fn_path_token, - &serde_path, + // for the server function structure + let (additional_path, derives) = additional_path_and_derives_tokens( + input_ident, + &input_derive, + &server_fn_path_token, + &serde_path, ); // for the associated types that are not PATH let client = client_tokens(&client, &server_fn_path_token); - let req = req_tokens(&server_fn_path_token, &req_ty, &preset_req); - let res = resp_tokens(&server_fn_path_token, &res_ty, &preset_res); - let output_ty = output_type(return_ty)?; - let input_encoding = - input_encoding_tokens(input, builtin_encoding, &server_fn_path); - let output_encoding = - output_encoding_tokens(output, builtin_encoding, &server_fn_path); - let error_ty = error_ty_tokens(return_ty, &server_fn_path)?; - - // only emit the dummy (unmodified server-only body) for the server build - let dummy = cfg!(feature = "ssr").then_some(dummy); + let req = req_tokens(&server_fn_path_token, &req_ty, &preset_req); + let res = resp_tokens(&server_fn_path_token, &res_ty, &preset_res); + let output_ty = output_type(return_ty)?; + let input_encoding = + input_encoding_tokens(input, builtin_encoding, &server_fn_path); + let output_encoding = + output_encoding_tokens(output, builtin_encoding, &server_fn_path); + let error_ty = error_ty_tokens(return_ty, &server_fn_path)?; + + // only emit the dummy (unmodified server-only body) for the server build + let dummy = cfg!(feature = "ssr").then_some(dummy); if let Some(register) = maybe_register { - let (impl_generics, ty_generics, where_clause) = body.generics.split_for_impl(); - - let func = func_tokens( - &docs, - attrs, - vis, - fn_name, - &fn_args, - output_arrow, - return_ty, - &dummy_name, - &field_names, - &custom_wrapper, - &struct_name, - &server_fn_path_token, - Some(&impl_generics), - Some(&ty_generics), - where_clause, - output_ty, - &error_ty, - ); + let (impl_generics, ty_generics, where_clause) = + body.generics.split_for_impl(); + + let input_types = input_types(&body); + + // let InputFieldTypes = {T ∈ InputFieldTypes | T is a type in the InputTypes of the OriginalServerFunction} + // let PhantomTypes = {U ∈ GenericParameters of OriginalServerFunction | U ∉ InputFieldTypes} + // If PhantomTypes is not empty, then we need to add a _marker field to our GenericServerFunctionStructure + // Whose type is PhantomData<(∀Ui​∈U)> + + // PhantomTypes are not types that end in the word Phantom. But types that are in PhantomData<...> lol + let phantom_types = { + let mut phantom_types = body + .generics + .type_params() + .map(|ty| ty.ident.clone()) + .collect::>(); + phantom_types.retain(|ty| !input_types.contains(ty)); + phantom_types + }; + let has_marker = !phantom_types.is_empty(); // for each register entry, we generate a unique implementation and inventory. let mut implementations_and_inventories = vec![]; - for RegisterEntry { - specific_ty, - endpoint, - } in register.into_registered_entries() + + for RegisterEntry { internal, endpoint } in + register.as_registered_entries() { - let g : Generics = parse_quote!(< #specific_ty >); - let (_,ty_generics,_) = g.split_for_impl(); + let specific_ty = Punctuated::::from_iter( + internal.iter().map(|i| i.specific_ty.clone()), + ); + let specific_ty_phantom_suffix_removed = specific_ty + .clone() + .into_iter() + .map(|ident| { + let ident_str = ident.to_string(); + if ident_str.ends_with("Phantom") + && ident_str.len() > "Phantom".len() + { + syn::Ident::new( + &ident_str[..ident_str.len() - "Phantom".len()], + ident.span(), + ) + } else { + ident + } + }) + .collect::>(); + let g: Generics = parse_quote!(< #specific_ty >); + let (_, ty_generics, _) = g.split_for_impl(); + let g: Generics = + parse_quote!(< #specific_ty_phantom_suffix_removed >); + let (_, ty_generics_phantom_suffix_removed, _) = g.split_for_impl(); // These will be the struct name with the specific generics for a given register arg // i.e register() -> #struct_name - let wrapped_struct_name = - possibly_wrap_struct_name(&struct_name, &custom_wrapper,Some(&ty_generics)); - let wrapped_struct_name_turbofish = - possibly_wrapped_struct_name_turbofish( + let wrapped_struct_name = possibly_wrap_struct_name( &struct_name, &custom_wrapper, Some(&ty_generics), ); - // auto-registration with inventory - let inventory = inventory_tokens( - &server_fn_path_token, - &wrapped_struct_name_turbofish, - &wrapped_struct_name, - ); - // let wrapped_struct_name include the specific_ty from RegisterEntry - - // let path = the endpoint, or else create the endpoint and stringify the specific_ty as part of the hashing. + let wrapped_struct_name_turbofish = + possibly_wrapped_struct_name_turbofish( + &struct_name, + &custom_wrapper, + Some(&ty_generics), + ); + // auto-registration with inventory + let inventory = inventory_tokens( + &server_fn_path_token, + &wrapped_struct_name_turbofish, + &wrapped_struct_name, + ); - // run_body in the trait implementation - let run_body = run_body_tokens( - &custom_wrapper, - &struct_name, - &field_names, - &dummy_name, - &server_fn_path_token, - return_ty, - ); - // the endpoint of our path should be specified in the register attribute if at all. - let endpoint = Literal::string(&endpoint.map(|e|e.value()).unwrap_or_default()); - let path = path_tokens( - &endpoint, - &server_fn_path_token, - &prefix, - &fn_name_as_str, - Some(&specific_ty) - ); - - let middlewares = middlewares_tokens(&middlewares); + let run_body = run_body_tokens( + &custom_wrapper, + &struct_name, + &field_names, + &dummy_name, + &server_fn_path_token, + return_ty, + has_marker, + Some(&ty_generics_phantom_suffix_removed), + ); + // the endpoint of our path should be specified in the register attribute if at all. + let endpoint = Literal::string( + &endpoint.map(|e| e.value()).unwrap_or_default(), + ); + let path = path_tokens( + &endpoint, + &server_fn_path_token, + &prefix, + &fn_name_as_str, + Some(&specific_ty), + ); + let middlewares = middlewares_tokens(&middlewares); implementations_and_inventories.push(quote!( impl #server_fn_path_token::ServerFn for #wrapped_struct_name { const PATH: &'static str = #path; - + type Client = #client; type ServerRequest = #req; type ServerResponse = #res; @@ -981,37 +1170,104 @@ pub fn server_macro_impl( type InputEncoding = #input_encoding; type OutputEncoding = #output_encoding; type Error = #error_ty; - + fn middlewares() -> Vec>> { #middlewares } - + #run_body } - + #inventory )) } - Ok(quote!( - #args_docs - #docs - #[derive(Debug, #derives)] - #additional_path - pub struct #struct_name #impl_generics #where_clause { - #(#fields),* + + let traits_that_should_be_constraints = + register.produce_trait_idents_that_should_be_constraints(); + let types_that_should_have_phantom_suffix = + register.produce_type_idents_that_should_have_phantom_suffix(); + let mut g = body.generics.clone(); + for ty in g.type_params_mut() { + if types_that_should_have_phantom_suffix.contains(&ty.ident) { + ty.ident = Ident::new( + &format!("{}Phantom", &ty.ident), + ty.ident.span(), + ); } - #(#implementations_and_inventories)* + ty.bounds = ty + .bounds + .clone() + .into_iter() + .map(|ty| { + if let TypeParamBound::Trait(ref trait_bound) = ty { + if let Some(ident) = trait_bound.path.get_ident() { + if traits_that_should_be_constraints.contains(ident) + { + let ident = Ident::new( + &format!("{ident}Constraint"), + Span::call_site(), + ); + let bound: TraitBound = parse_quote!(#ident); + TypeParamBound::Trait(bound) + } else { + ty + } + } else { + ty + } + } else { + ty + } + }) + .collect::>(); + } + // this will include the defaults for the generic + let params_with_trait_constraints = g.params.clone(); + let (impl_generics_with_trait_constraints, _, _) = g.split_for_impl(); + let fields = extend_fields_with_phantom_types(fields, phantom_types); + let func = func_tokens( + &docs, + attrs, + vis, + fn_name, + &fn_args, + output_arrow, + return_ty, + &dummy_name, + &field_names, + &custom_wrapper, + &struct_name, + &server_fn_path_token, + Some(&impl_generics), + Some(&impl_generics_with_trait_constraints), + Some(&ty_generics), + where_clause, + output_ty, + &error_ty, + has_marker, + ); + + Ok(quote!( + #args_docs + #docs + #[derive(Debug, #derives)] + #additional_path + pub struct #struct_name < #params_with_trait_constraints > #where_clause { + #(#fields),* + } - #func + #(#implementations_and_inventories)* - #dummy - - )) + #func + + #dummy + + )) } else { // struct name, wrapped in any custom-encoding newtype wrapper (if it exists, otherwise it's just the struct name) let wrapped_struct_name = - possibly_wrap_struct_name(&struct_name, &custom_wrapper,None); + possibly_wrap_struct_name(&struct_name, &custom_wrapper, None); let wrapped_struct_name_turbofish = possibly_wrapped_struct_name_turbofish( &struct_name, @@ -1019,11 +1275,8 @@ pub fn server_macro_impl( None, ); - - let from_impl = impl_from_tokens(&body, impl_from, &struct_name); - // auto-registration with inventory let inventory = inventory_tokens( &server_fn_path_token, @@ -1039,6 +1292,8 @@ pub fn server_macro_impl( &dummy_name, &server_fn_path_token, return_ty, + false, + None, ); // the actual function definition @@ -1055,23 +1310,23 @@ pub fn server_macro_impl( &custom_wrapper, &struct_name, &server_fn_path_token, - None,None,None, + None, + None, + None, + None, output_ty, - &error_ty + &error_ty, + false, ); - - let path = path_tokens( &endpoint, &server_fn_path_token, &prefix, &fn_name_as_str, - None + None, ); - - let middlewares = middlewares_tokens(&middlewares); Ok(quote::quote! { @@ -1151,32 +1406,109 @@ impl Parse for Middleware { #[derive(Debug, Clone)] struct Register { /// Matches something like: ( = "some_string", , ...) - type_value_pairs: - Punctuated<(Punctuated, Option), Token![,]>, + type_value_pairs: Punctuated< + (Punctuated, Option), + Token![,], + >, +} + +impl Register { + fn produce_trait_idents_that_should_be_constraints(&self) -> Vec { + let mut result = Vec::new(); + for (entry, _) in self.type_value_pairs.iter() { + for internal_entry in entry { + if let Some(ref trait_list) = internal_entry.maybe_trait_list { + for trait_ident in trait_list { + let ident_str = trait_ident.to_string(); + if ident_str.ends_with("Constraint") + && ident_str.len() > "Constraint".len() + { + result.push(Ident::new( + &ident_str + [0..&ident_str.len() - "Constraint".len()], + trait_ident.span(), + )); + } + } + } + } + } + result + } + fn produce_type_idents_that_should_have_phantom_suffix( + &self, + ) -> Vec { + let mut result = Vec::new(); + for (entry, _) in self.type_value_pairs.iter() { + for internal_entry in entry { + let ty = internal_entry.specific_ty.clone(); + let ident_str = ty.to_string(); + if ident_str.ends_with("Phantom") + && ident_str.len() > "Phantom".len() + { + result.push(Ident::new( + &ident_str[0..&ident_str.len() - "Phantom".len()], + ty.span(), + )); + } + } + } + result + } +} +/// When we use PhantomTypes for our backends we can specify that they are over Trait constraints which are shadows for our ssr only traits. +/// ```rust,ignore +/// #[server] +/// #[register()] +/// pub async fn example::() -> Result<(),ServerFnError> { +/// // ... +/// } +/// ``` +#[derive(Debug, Clone)] +struct InternalRegisterEntry { + specific_ty: Ident, + maybe_colon: Option, + maybe_trait_list: Option>, +} +impl Parse for InternalRegisterEntry { + fn parse(input: ParseStream) -> Result { + let specific_ty = input.parse::()?; + let maybe_colon = if input.peek(Token![:]) { + Some(input.parse::()?) + } else { + None + }; + if maybe_colon.is_none() { + Ok(Self { + specific_ty, + maybe_colon, + maybe_trait_list: None, + }) + } else { + let maybe_trait_list = + Some(Punctuated::::parse_separated_nonempty( + input, + )?); + return Ok(Self { + specific_ty, + maybe_colon, + maybe_trait_list, + }); + } + } } struct RegisterEntry { - specific_ty: Punctuated, + internal: Punctuated, endpoint: Option, } impl Register { - fn into_registered_entries(self) -> Vec { + fn as_registered_entries(&self) -> Vec { self.type_value_pairs + .clone() .into_iter() - .map(|(specific_ty, endpoint)| RegisterEntry { - specific_ty, - endpoint, - }) + .map(|(internal, endpoint)| RegisterEntry { internal, endpoint }) .collect() } - /// If any specific registered type in the register attribute ends with the word Phantom and we have enabled ssr_generics then - /// we have backend generics - fn has_backend_generics(&self) -> bool { - self.type_value_pairs.iter().any(|(specific_ty, _)| { - specific_ty - .iter() - .any(|ty| ty.to_string().ends_with("Phantom")) - }) && cfg!(feature = "ssr_generics") - } } impl Parse for Register { fn parse(input: ParseStream) -> Result { @@ -1186,7 +1518,7 @@ impl Parse for Register { // Parse the angle bracketed ident list input.parse::()?; let idents = - Punctuated::::parse_separated_nonempty( + Punctuated::::parse_separated_nonempty( input, )?; input.parse::]>()?; @@ -1223,7 +1555,7 @@ impl Parse for Register { for (p, _) in ®ister.type_value_pairs { if p.len() != expected_len { return Err(syn::Error::new( - p.span(), + Span::call_site(), "All bracketed lists must have the same length in register", )); } @@ -1654,7 +1986,7 @@ impl ServerFnBody { .. } = &self; // will all be empty if no generics... - let (impl_generics,_,where_clause) = generics.split_for_impl(); + let (impl_generics, _, where_clause) = generics.split_for_impl(); quote! { #[doc(hidden)] #(#attrs)* From f2d76687ca0724661b0dcde2054468a2dc17b18b Mon Sep 17 00:00:00 2001 From: Sam <@> Date: Sat, 21 Dec 2024 15:37:58 -0500 Subject: [PATCH 4/6] Generic result T --- examples/server_fns_axum/src/app.rs | 22 ++++++++++-- server_fn_macro/src/lib.rs | 53 ++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/examples/server_fns_axum/src/app.rs b/examples/server_fns_axum/src/app.rs index 1310870bda..351b47f317 100644 --- a/examples/server_fns_axum/src/app.rs +++ b/examples/server_fns_axum/src/app.rs @@ -76,10 +76,9 @@ pub fn HomePage() -> impl IntoView {

"Generic Server Functions"

- - + - + } } @@ -1161,3 +1160,20 @@ pub fn GenericSsrOnlyTypes() -> impl IntoView { } } + +#[server] +#[register(,)] +pub async fn generic_default_result() -> Result { + Ok(R::default()) +} + +#[component] +pub fn GenericServerFnResult() -> impl IntoView { + Suspend::new(async move { + format!( + " Default u8 is {} \n Default String is {}", + generic_default_result::().await.unwrap(), + generic_default_result::().await.unwrap(), + ) + }) +} diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 50fc1d82fc..d1f2092e7e 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -468,7 +468,8 @@ fn run_body_tokens( field_names: &Vec<&Box>, dummy_name: &Ident, server_fn_path: &TokenStream2, - return_ty: &Type, + output_ty: &GenericArgument, + error_ty: &TokenStream2, has_marker: bool, specific_ty_phantom_suffix_removed: Option<&TypeGenerics>, ) -> TokenStream2 { @@ -534,14 +535,14 @@ fn run_body_tokens( // 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 { + fn run_body(self) -> impl std::future::Future>> + Send { #body } } } else { quote! { #[allow(unused_variables)] - async fn run_body(self) -> #return_ty { + async fn run_body(self) -> Result< #output_ty, #server_fn_path::error::ServerFnError< #error_ty >> { unreachable!() } } @@ -1093,6 +1094,11 @@ pub fn server_macro_impl( let specific_ty = Punctuated::::from_iter( internal.iter().map(|i| i.specific_ty.clone()), ); + let output_ty = canonicalize_output_type_generic( + &parse_quote!(#ty_generics), + &specific_ty, + output_ty, + ); let specific_ty_phantom_suffix_removed = specific_ty .clone() .into_iter() @@ -1141,7 +1147,8 @@ pub fn server_macro_impl( &field_names, &dummy_name, &server_fn_path_token, - return_ty, + &output_ty, + &error_ty, has_marker, Some(&ty_generics_phantom_suffix_removed), ); @@ -1291,7 +1298,8 @@ pub fn server_macro_impl( &field_names, &dummy_name, &server_fn_path_token, - return_ty, + &output_ty, + &error_ty, false, None, ); @@ -1564,6 +1572,7 @@ impl Parse for Register { Ok(register) } } + fn output_type(return_ty: &Type) -> Result<&GenericArgument> { if let syn::Type::Path(pat) = &return_ty { if pat.path.segments[0].ident == "Result" { @@ -1584,6 +1593,40 @@ fn output_type(return_ty: &Type) -> Result<&GenericArgument> { )) } +/// In our ServerFn implementation, when we canonocalize the generic of our server function to our specific registed types +/// if one of those types appear as the T in Result then we need to canonicalize the output type as well. +fn canonicalize_output_type_generic( + generics: &Generics, + specific_ty: &Punctuated, + output_type: &GenericArgument, +) -> GenericArgument { + match output_type { + GenericArgument::Lifetime(lifetime) => todo!(), + GenericArgument::Type(ty) => { + // if generics include the output type, then the output type is generic. + if let Type::Path(type_path) = ty { + if let Some(pos) = generics.type_params().position(|inner_ty| { + Some(&inner_ty.ident) == type_path.path.get_ident() + }) { + let normal_form = specific_ty.get(pos).expect( + "Specific types should have length of generics", + ); + GenericArgument::Type(parse_quote!(#normal_form)) + } else { + output_type.clone() + } + } else { + output_type.clone() + } + } + GenericArgument::Const(expr) => todo!(), + GenericArgument::AssocType(assoc_type) => todo!(), + GenericArgument::AssocConst(assoc_const) => todo!(), + GenericArgument::Constraint(constraint) => todo!(), + _ => todo!(), + } +} + fn err_type(return_ty: &Type) -> Result> { if let syn::Type::Path(pat) = &return_ty { if pat.path.segments[0].ident == "Result" { From 00a813945517404a4758015ea38da9a1128d3ad8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:13:46 +0000 Subject: [PATCH 5/6] [autofix.ci] apply automated fixes --- server_fn_macro/src/lib.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index d1f2092e7e..8b398ae3de 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -56,7 +56,7 @@ fn extract_register(body: &mut ServerFnBody) -> Result> { "cannot use more than 1 register attribute", )); } - Ok(register.get(0).cloned()) + Ok(register.first().cloned()) } /// Takes body, and returns a list of field types to compare to. @@ -338,13 +338,11 @@ fn possibly_wrapped_struct_name_turbofish( } else { quote! { #wrapper::<#struct_name> } } + } else if let Some(ty_generics) = ty_generics { + let ty_generics = ty_generics.as_turbofish(); + quote! { #struct_name #ty_generics } } else { - if let Some(ty_generics) = ty_generics { - let ty_generics = ty_generics.as_turbofish(); - quote! { #struct_name #ty_generics } - } else { - quote! { #struct_name #ty_generics } - } + quote! { #struct_name #ty_generics } } } @@ -482,14 +480,12 @@ fn run_body_tokens( let mut field_names = field_names.clone(); field_names.push(&marker); field_names - .into_iter() - .map(|t| t.clone()) + .into_iter().cloned() .collect::>() } else { field_names .clone() - .into_iter() - .map(|t| t.clone()) + .into_iter().cloned() .collect::>() } }; @@ -1298,7 +1294,7 @@ pub fn server_macro_impl( &field_names, &dummy_name, &server_fn_path_token, - &output_ty, + output_ty, &error_ty, false, None, @@ -1497,11 +1493,11 @@ impl Parse for InternalRegisterEntry { Some(Punctuated::::parse_separated_nonempty( input, )?); - return Ok(Self { + Ok(Self { specific_ty, maybe_colon, maybe_trait_list, - }); + }) } } } From 33338469889f938fe4a5a6f5c12814f5ac55096f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 21 Dec 2024 21:26:53 +0000 Subject: [PATCH 6/6] [autofix.ci] apply automated fixes (attempt 2/3) --- server_fn_macro/src/lib.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 8b398ae3de..1d1ae2cab5 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -479,14 +479,9 @@ fn run_body_tokens( if has_marker { let mut field_names = field_names.clone(); field_names.push(&marker); - field_names - .into_iter().cloned() - .collect::>() + field_names.into_iter().cloned().collect::>() } else { - field_names - .clone() - .into_iter().cloned() - .collect::>() + field_names.clone().into_iter().cloned().collect::>() } }; let destructure = if let Some(wrapper) = custom_wrapper.as_ref() {