-
-
Notifications
You must be signed in to change notification settings - Fork 695
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
base: main
Are you sure you want to change the base?
Generic Server Fn #3397
Changes from all commits
3864517
ccc51f1
def448b
9480b8b
62a2f2f
f2d7668
00a8139
3333846
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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/> | ||
|
@@ -72,6 +73,12 @@ pub fn HomePage() -> impl IntoView { | |
<FileWatcher/> | ||
<CustomEncoding/> | ||
<CustomClientExample/> | ||
<h2>"Generic Server Functions"</h2> | ||
<SimpleGenericServerFnComponent/> | ||
<GenericHelloWorld/> | ||
<GenericHelloWorldWithDefaults/> | ||
<GenericSsrOnlyTypes/> | ||
<GenericServerFnResult/> | ||
} | ||
} | ||
|
||
|
@@ -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>)] | ||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
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
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise the addition of the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
and actually creates two structures 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>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I would prefer the first option for discoverability/"go to definition" reasons, personally. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
) | ||
.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(), | ||
) | ||
}) | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:)