Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic Server Fn #3397

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/server_fns_axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ server_fn = { path = "../../server_fn", features = [
"rkyv",
"multipart",
"postcard",
"ssr_generics",
] }
log = "0.4.22"
simple_logger = "5.0"
Expand Down
234 changes: 233 additions & 1 deletion examples/server_fns_axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ pub fn App() -> impl IntoView {
#[component]
pub fn HomePage() -> impl IntoView {
view! {
<h2>"Some Simple Server Functions"</h2>

<h2>"Some Simple Server Functions"</h2>
<SpawnLocal/>
<WithAnAction/>
<WithActionForm/>
Expand All @@ -72,6 +73,12 @@ pub fn HomePage() -> impl IntoView {
<FileWatcher/>
<CustomEncoding/>
<CustomClientExample/>
<h2>"Generic Server Functions"</h2>
<SimpleGenericServerFnComponent/>
<GenericHelloWorld/>
<GenericHelloWorldWithDefaults/>
<GenericSsrOnlyTypes/>
<GenericServerFnResult/>
}
}

Expand Down Expand Up @@ -945,3 +952,228 @@ pub fn PostcardExample() -> impl IntoView {
</Transition>
}
}

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(<String>,<u8>)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#[register] seems like a reasonable way to do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:)

pub async fn server_fn_to_string<S: Display>(
s: S,
) -> Result<String, ServerFnError> {
println!("Type {} got arg {s}", std::any::type_name::<S>());
Ok(format!("{s}"))
}

#[component]
pub fn SimpleGenericServerFnComponent() -> impl IntoView {
let string_to_string = Resource::new(
|| (),
|_| server_fn_to_string(String::from("I'm a String.")),
);
let u8_to_string = Resource::new(
move || (),
|_| async move { server_fn_to_string::<u8>(42).await },
);
view! {
<h3>Using generic function over display</h3>
<p>"This example demonstrates creating a generic function that takes any type that implements display that we've registered."</p>

<ErrorBoundary fallback=move |err|leptos::logging::log!("{err:?}")>

<Suspense >
<h4>Result 1</h4>
<p>
{
move || string_to_string.get()
}
</p>
<h4>Result 2</h4>
<p>
{
move || u8_to_string.get()
}
</p>
</Suspense>
</ErrorBoundary >


}
}

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(<AStruct>,<AStruct2>)]
pub async fn server_hello_world_generic<S: SomeTrait>(
) -> Result<String, ServerFnError> {
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::<AStruct>().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::<AStruct2>().await {
Ok(hello) => s2.set(hello),
Err(err) => leptos::logging::log!("{err:?}"),
}
})
});
view! {
<h3>Using generic functions without generic inputs</h3>
<p>"This example demonstrates creating a generic server function that doesn't take any generic input."</p>
<h4>{"Results"}</h4>
<p>{ move || format!("With generic specified to {} we get {} from the server", std::any::type_name::<AStruct>(), s.get())}</p>
<p>{ move || format!("With generic specified to {} we get {} from the server", std::any::type_name::<AStruct2>(), s2.get())}</p>

}
}

#[derive(Clone)]
struct SomeDefault;
impl SomeTrait for SomeDefault {
fn some_method() -> String {
String::from("just a default hello world...")
}
}

#[server]
#[register(<SomeDefault>)]
pub async fn server_hello_world_generic_with_default<
S: SomeTrait = SomeDefault,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me that the "default generic" approach is working here — for example, if I try to call it as a function

let x = server_hello_world_generic_with_default();

The compiler errors

error[E0283]: type annotations needed
    --> src/app.rs:1077:13
     |
1077 |     let x = server_hello_world_generic_with_default();
     |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type of the type parameter `S` declared on the function `server_hello_world_generic_with_default`
     |
     = note: cannot satisfy `_: SomeTrait`
     = help: the following types implement trait `SomeTrait`:
               AStruct
               AStruct2
               SomeDefault

Looking at the macro expansion, it looks like the default is not added on the function

pub async fn server_hello_world_generic_with_default<S: SomeTrait>(
) -> Result<String, ServerFnError> {
    __server_hello_world_generic_with_default::<S>().await
}

but of course, that's because you can't give default values to generics on functions

fn something<S: SomeTrait = SomeDefault>() {}

gives the error

error: defaults for type parameters are only allowed in `struct`, `enum`, `type`, or `trait` definitions
    --> src/app.rs:1075:14
     |
1075 | fn something<S: SomeTrait = SomeDefault>() {}
     |              ^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
     = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
     = note: for more information, see issue #36887 <https://github.com/rust-lang/rust/issues/36887>
     = note: `#[deny(invalid_type_param_default)]` on by default

From my perspective, this just makes it confusing: Server functions are intended as an abstraction over functions, but Rust functions don't allow default generics; so now an invalid syntax for a function (including a default generic) is accepted by the macro but stripped from the expansion.

I'll put it this way: I know Rust fairly well, and I did not know any of the above until I tested it. I think this is likely to lead to more user confusion than benefit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I kind of tacked default on at the last minute as a "wouldn't this be cool" kind of feature. I came up against no default generic functions, I think I must have added it for server function structures but forget to try it against functions (I might have not gone ahead with it if I had lol). I'm not sure this feature makes sense for the reasons you stated i.e given that generics can't be default in server functions, and since the server function struct is where they'd have to be default in but that's not a given for any particular server function it is kind of confusing etc.

>() -> Result<String, ServerFnError> {
Ok(S::some_method())
}

#[component]
pub fn GenericHelloWorldWithDefaults() -> impl IntoView {
let action = ServerAction::<ServerHelloWorldGenericWithDefault>::new();
Effect::new(move |_| {
action.dispatch(ServerHelloWorldGenericWithDefault {
_marker: std::marker::PhantomData,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise the addition of the _marker field here is confusing, I think — this name doesn't come from anywhere, so if I'm the user I don't know that it exists or how to tweak the name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be called _phantom instead?

We need PhantomData for specifying the generic types of the server functions when they otherwise aren't present. I could default to _phantom for field names and add an arg the ServerFnArg that let you specify an alternate _phantom field. My thinking here is that people could write

action.dispatch(ServerFnStruct{
arg1,arg2,..Default::default()
})

Maybe we should try to implement default for server fn structures with generic arguments that would otherwise require Phantomdata. But then again, you lose some of the strengths of your type system when you can then start to accidentally pass default arguments to a server function that would really prefer the real thing. Might be hard to debug...

But otherwise I think the existence of PhantomData would have to be communicated to the user via the documentation. Since I don't think there's an ergonomic way around using PhantomData here, alternatively we could propagate the generic canoncalization process to the naming convention (lol sorry) for the structure so for instance where we have

#[regsister(<String>,<u8>)]
async server_fn_name<T>() -> ...

and actually creates two structures
ServerFnName_String ServerFnName_u8, both without arguments.

But that's definitely worse.

});
});

view! {
<h3>Using generic functions without generic inputs but a specified default type</h3>
<p>"This example demonstrates creating a generic server function that doesn't take any generic input and has a default generic type."</p>
<h4>{"Results"}</h4>
<p>{ move || action.value_local().read_only().get().map(|s|s.map(|s|format!("With default generic we get {s} from the server", )))}</p>
}
}

#[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(<ServerOnlyStructPhantom:ServerOnlyTraitConstraint>,
<SsrOnlyStructButDifferentPhantom:ServerOnlyTraitConstraint>
)]
pub async fn generic_server_with_ssr_only_types<T: ServerOnlyTrait>(
) -> Result<String, ServerFnError> {
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::<ServerOnlyStructPhantom>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it might be better to either 1) allow the user to specify the name of the phantom struct in ssr_type_shim! or 2) rename this from ___Phantom to something more like ___FromClient.

I would prefer the first option for discoverability/"go to definition" reasons, personally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can do that, would you prefer a mixture of both or to force the user to name it.
The mixture would consume an argument and override the default of ___FromClient

)
.await
{
Ok(hello) => s.set(hello),
Err(err) => leptos::logging::log!("{err:?}"),
}
});
});

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
{
Ok(hello) => s2.set(hello),
Err(err) => leptos::logging::log!("{err:?}"),
}
});
});
view! {
<h3>Using generic functions with a type that only exists on the server.</h3>
<p>"This example demonstrates how to make use of the helper macros and phantom types to make your backend generic and specifiable from your frontend."</p>
<h4>{"Results"}</h4>
<p>{ move || format!("With backend 1 we get {} from the server", s.get())}</p>
<p>{ move || format!("With backend 2 we get {} from the server", s2.get())}</p>

}
}

#[server]
#[register(<String>,<u8>)]
pub async fn generic_default_result<R: Default>() -> Result<R, ServerFnError> {
Ok(R::default())
}

#[component]
pub fn GenericServerFnResult() -> impl IntoView {
Suspend::new(async move {
format!(
" Default u8 is {} \n Default String is {}",
generic_default_result::<String>().await.unwrap(),
generic_default_result::<u8>().await.unwrap(),
)
})
}
3 changes: 3 additions & 0 deletions server_fn/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions server_fn/server_fn_macro_default/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`
/// 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(
/// <SpecificT,DefaultU>,
/// <OtherSpecificT,NotDefaultU>="other_struct_has_specific_route"
/// )]
/// pub async fn my_generic_server_fn<T : SomeTrait, U = DefaultU>(input:T) -> Result<(), ServerFnError>
/// where
/// U: ThisTraitIsInAWhereClause
/// {
/// todo!()
/// }
///
/// // expands to
/// #[derive(Deserialize, Serialize)]
/// struct MyGenericServerFn<T,U>
/// where
/// // we require these traits always for generic fn input
/// T : SomeTrait + Send + Serialize + DeserializeOwned + 'static,
/// U : ThisTraitIsInAWhereClause {
/// _marker:PhantomData<U>
/// input: T
/// }
///
/// impl ServerFn for MyGenericServerFn<SpecificT,DefaultU> {
/// // where our endpoint will be generated for us and unique to this type
/// const PATH: &'static str = "/api/...generated_endpoint...";
/// // ...
/// }
///
/// impl ServerFn for MyGenericServerFn<OtherSpecificT,NotDefaultU> {
/// 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(<BackendTypePhantom,DefaultU>)]
/// pub async fn generic_fn<T:BackendTrait,U = DefaultU>() -> Result<U,ServerFnError> {
/// todo!()
/// }
///
/// // expands to
/// #[derive(Deserialize, Serialize)]
/// struct GenericFc<T,U>
/// where
/// T : BackendTraitConstraint, {
/// _marker:PhantomData<(T,U)>
/// }
///
/// impl ServerFn for GenericFn<BackendTypePhantom,DefaultU> {
/// // 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(
Expand Down
6 changes: 6 additions & 0 deletions server_fn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ 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;
Expand All @@ -126,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)]
Expand Down
Loading
Loading