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

Added support for generics in server fns #3008

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
54 changes: 46 additions & 8 deletions examples/server_fns_axum/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub fn HomePage() -> impl IntoView {
view! {
<h2>"Some Simple Server Functions"</h2>
<SpawnLocal/>
<Generic/>
<WithAnAction/>
<WithActionForm/>
<h2>"Custom Error Types"</h2>
Expand All @@ -75,6 +76,44 @@ pub fn HomePage() -> impl IntoView {
}
}

/// Server functions can be made generic, which will register multiple endpoints.
///
/// If you use generics, you need to explicitly register the server function endpoint for each type
/// with [`server_fn::axum::register_explicit`] or [`server_fn::actix::register_explicit`]
#[component]
pub fn Generic() -> impl IntoView {
use std::fmt::Display;

#[server]
pub async fn test_fn<S>(input: S) -> Result<String, ServerFnError>
where
S: Display,
{
// insert a simulated wait
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
Ok(input.to_string())
}

view! {
<h3>Generic Server Functions</h3>
<p>"Server functions can be made generic, which will register multiple endpoints."</p>
<p>
"If you use generics, you need to explicitly register the server function endpoint for each type."
</p>
<p>"Open your browser devtools to see which endpoints the function below calls."</p>
<button on:click=move |_| {
spawn_local(async move {
test_fn("foo".to_string()).await.unwrap();
test_fn(42).await.unwrap();
test_fn(10.16).await.unwrap();
});
}>

"Click me and check the requests"
</button>
}
}

/// A server function is really just an API call to your server. But it provides a plain async
/// function as a wrapper around that. This means you can call it like any other async code, just
/// by spawning a task with `spawn_local`.
Expand Down Expand Up @@ -382,7 +421,8 @@ pub fn FileUpload() -> impl IntoView {
</form>
<p>
{move || {
if upload_action.input_local().read().is_none() && upload_action.value().read().is_none()
if upload_action.input_local().read().is_none()
&& upload_action.value().read().is_none()
{
"Upload a file.".to_string()
} else if upload_action.pending().get() {
Expand Down Expand Up @@ -929,13 +969,11 @@ pub fn PostcardExample() -> impl IntoView {
<h3>Using <code>postcard</code> encoding</h3>
<p>"This example demonstrates using Postcard for efficient binary serialization."</p>
<button on:click=move |_| {
// Update the input data when the button is clicked
set_input.update(|data| {
data.age += 1;
});
}>
"Increment Age"
</button>
set_input
.update(|data| {
data.age += 1;
});
}>"Increment Age"</button>
// Display the current input data
<p>"Input: " {move || format!("{:?}", input.get())}</p>
<Transition>
Expand Down
16 changes: 8 additions & 8 deletions leptos_macro/tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub mod tests {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH
<MyServerAction as ServerFn>::url()
.trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
Expand All @@ -30,7 +30,7 @@ pub mod tests {
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(<FooBar as ServerFn>::url(), "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
Expand All @@ -43,7 +43,7 @@ pub mod tests {
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/foo/bar/my_path");
assert_eq!(<FooBar as ServerFn>::url(), "/foo/bar/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::Cbor>()
Expand All @@ -56,7 +56,7 @@ pub mod tests {
pub async fn my_server_action() -> Result<(), ServerFnError> {
Ok(())
}
assert_eq!(<FooBar as ServerFn>::PATH, "/api/my_path");
assert_eq!(<FooBar as ServerFn>::url(), "/api/my_path");
assert_eq!(
TypeId::of::<<FooBar as ServerFn>::InputEncoding>(),
TypeId::of::<codec::PostUrl>()
Expand All @@ -70,7 +70,7 @@ pub mod tests {
Ok(())
}
assert_eq!(
<FooBar as ServerFn>::PATH.trim_end_matches(char::is_numeric),
<FooBar as ServerFn>::url().trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
assert_eq!(
Expand All @@ -86,7 +86,7 @@ pub mod tests {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH
<MyServerAction as ServerFn>::url()
.trim_end_matches(char::is_numeric),
"/foo/bar/my_server_action"
);
Expand All @@ -103,7 +103,7 @@ pub mod tests {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH
<MyServerAction as ServerFn>::url()
.trim_end_matches(char::is_numeric),
"/api/my_server_action"
);
Expand All @@ -120,7 +120,7 @@ pub mod tests {
Ok(())
}
assert_eq!(
<MyServerAction as ServerFn>::PATH,
<MyServerAction as ServerFn>::url(),
"/api/path/to/my/endpoint"
);
assert_eq!(
Expand Down
4 changes: 2 additions & 2 deletions leptos_server/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ where
#[track_caller]
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
(error.path() == S::url())
.then(|| ServerFnError::<S::Error>::de(error.err()))
.map(Err)
});
Expand Down Expand Up @@ -145,7 +145,7 @@ where
/// Creates a new [`Action`] that will call the server function `S` when dispatched.
pub fn new() -> Self {
let err = use_context::<ServerActionError>().and_then(|error| {
(error.path() == S::PATH)
(error.path() == S::url())
.then(|| ServerFnError::<S::Error>::de(error.err()))
.map(Err)
});
Expand Down
29 changes: 13 additions & 16 deletions server_fn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@
//! ad hoc HTTP API endpoint, not a magic formula. Any server function can be accessed by any HTTP
//! client. You should take care to sanitize any data being returned from the function to ensure it
//! does not leak data that should exist only on the server.
//! - **Server functions can’t be generic.** Because each server function creates a separate API endpoint,
//! it is difficult to monomorphize. As a result, server functions cannot be generic (for now?) If you need to use
//! a generic function, you can define a generic inner function called by multiple concrete server functions.
//! - **Generic server fns must be explicitly registered with the type.** Each server function creates
//! a separate API endpoint, which means that the URL can change depending on the generic type. As a
//! result, server functions that are generic must be explicitly registered with the
//! [`axum::register_explicit`] or [`actix::register_explicit`] function call with your generic type
//! passed into it as an argument.
//! - **Arguments and return types must be serializable.** We support a variety of different encodings,
//! but one way or another arguments need to be serialized to be sent to the server and deserialized
//! on the server, and the return type must be serialized on the server and deserialized on the client.
Expand Down Expand Up @@ -191,9 +193,6 @@ where
Self::Error,
>,
{
/// A unique path for the server function’s API endpoint, relative to the host, including its prefix.
const PATH: &'static str;

/// The type of the HTTP client that will send the request from the client side.
///
/// For example, this might be `gloo-net` in the browser, or `reqwest` for a desktop app.
Expand Down Expand Up @@ -227,9 +226,7 @@ where
type Error: FromStr + Display;

/// Returns [`Self::PATH`].
rakshith-ravi marked this conversation as resolved.
Show resolved Hide resolved
fn url() -> &'static str {
Self::PATH
}
fn url() -> &'static str;

/// Middleware that should be applied to this server function.
fn middlewares(
Expand Down Expand Up @@ -265,7 +262,7 @@ where
.map(|res| (res, None))
.unwrap_or_else(|e| {
(
Self::ServerResponse::error_response(Self::PATH, &e),
Self::ServerResponse::error_response(Self::url(), &e),
Some(e),
)
});
Expand All @@ -275,7 +272,7 @@ where
if accepts_html {
// if it had an error, encode that error in the URL
if let Some(err) = err {
if let Ok(url) = ServerFnUrlError::new(Self::PATH, err)
if let Ok(url) = ServerFnUrlError::new(Self::url(), err)
.to_url(referer.as_deref().unwrap_or("/"))
{
referer = Some(url.to_string());
Expand Down Expand Up @@ -303,7 +300,7 @@ where
async move {
// create and send request on client
let req =
self.into_req(Self::PATH, Self::OutputEncoding::CONTENT_TYPE)?;
self.into_req(Self::url(), Self::OutputEncoding::CONTENT_TYPE)?;
Self::run_on_client_with_req(req, redirect::REDIRECT_HOOK.get())
.await
}
Expand Down Expand Up @@ -489,9 +486,9 @@ pub mod axum {
> + 'static,
{
REGISTERED_SERVER_FUNCTIONS.insert(
(T::PATH.into(), T::InputEncoding::METHOD),
(T::url().into(), T::InputEncoding::METHOD),
ServerFnTraitObj::new(
T::PATH,
T::url(),
T::InputEncoding::METHOD,
|req| Box::pin(T::run_on_server(req)),
T::middlewares,
Expand Down Expand Up @@ -577,9 +574,9 @@ pub mod actix {
> + 'static,
{
REGISTERED_SERVER_FUNCTIONS.insert(
(T::PATH.into(), T::InputEncoding::METHOD),
(T::url().into(), T::InputEncoding::METHOD),
ServerFnTraitObj::new(
T::PATH,
T::url(),
T::InputEncoding::METHOD,
|req| Box::pin(T::run_on_server(req)),
T::middlewares,
Expand Down
Loading
Loading