Repository: leptos-rs/leptos
Pull request #2335 (2024-02-17 23:28:57)

feat: add `impl_from` argument to `#[server]` proc_macro
This PR adds an optional `impl_from` named  argument for `server_fn` proc macro.
This argument defaults to `true` and enables (or disables) the impl of `From` trait for server function argument type.

In cases when the server function has more than one argument, the `impl_from` has no effect on the trait impl generation, as it has been already disabled by function arguments number check.

The related issue has been discussed here: https://discord.com/channels/1031524867910148188/1203001939089035295

TL;DR: orphan rules disable (in some cases) the implementation of `From` trait for conversion between the only server function argument and server function argument type. 

The minimal example of the issue lives here: https://github.com/videobitva/test_server_fn



Patch:
diff --git a/Cargo.toml b/Cargo.toml
index cb10f4cd..e0a33375 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,7 +25,7 @@ members = [
 exclude = ["benchmarks", "examples"]
 
 [workspace.package]
-version = "0.6.5"
+version = "0.6.6"
 
 [workspace.dependencies]
 leptos = { path = "./leptos", version = "0.6.5" }
diff --git a/leptos/Cargo.toml b/leptos/Cargo.toml
index 2e718b88..d56d618f 100644
--- a/leptos/Cargo.toml
+++ b/leptos/Cargo.toml
@@ -15,7 +15,7 @@ leptos_macro = { workspace = true }
 leptos_reactive = { workspace = true }
 leptos_server = { workspace = true }
 leptos_config = { workspace = true }
-leptos-spin-macro = { git = "https://github.com/fermyon/leptos-spin", optional = true}
+leptos-spin-macro = { version="0.1", optional = true}
 tracing = "0.1"
 typed-builder = "0.18"
 typed-builder-macro = "0.18"
diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs
index dc949643..6beebf32 100644
--- a/leptos_macro/src/lib.rs
+++ b/leptos_macro/src/lib.rs
@@ -883,6 +883,11 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
 ///     - `"GetCbor"`: `GET` request with URL-encoded arguments and CBOR response
 /// - `req` and `res` specify the HTTP request and response types to be used on the server (these
 ///   should usually only be necessary if you are integrating with a server other than Actix/Axum)
+/// - `impl_from`: specifies whether to implement trait `From` for server function's type or not.
+///   By default, if a server function only has one argument, the macro automatically implements the `From` trait
+///   to convert from the argument type to the server function type, and vice versa, allowing you to convert
+///   between them easily. Setting `impl_from` to `false` disables this, which can be necessary for argument types
+///   for which this would create a conflicting implementation. (defaults to `true`)
 ///
 /// ```rust,ignore
 /// #[server(
@@ -891,6 +896,7 @@ pub fn slot(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
 ///   endpoint = "my_fn",
 ///   input = Cbor,
 ///   output = Json
+///   impl_from = true
 /// )]
 /// pub async fn my_wacky_server_fn(input: Vec<String>) -> Result<usize, ServerFnError> {
 ///   todo!()
diff --git a/leptos_reactive/src/memo.rs b/leptos_reactive/src/memo.rs
index 858305a8..760a38d2 100644
--- a/leptos_reactive/src/memo.rs
+++ b/leptos_reactive/src/memo.rs
@@ -86,7 +86,88 @@ pub fn create_memo<T>(f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
 where
     T: PartialEq + 'static,
 {
-    Runtime::current().create_memo(f)
+    Runtime::current().create_owning_memo(move |current_value| {
+        let new_value = f(current_value.as_ref());
+        let is_different = current_value.as_ref() != Some(&new_value);
+        (new_value, is_different)
+    })
+}
+
+/// Like [`create_memo`], `create_owning_memo` creates an efficient derived reactive value based on
+/// other reactive values, but with two differences:
+/// 1. The argument to the memo function is owned instead of borrowed.
+/// 2. The function must also return whether the value has changed, as the first element of the tuple.
+///
+/// All of the other caveats and guarantees are the same as the usual "borrowing" memos.
+///
+/// This type of memo is useful for memos which can avoid computation by re-using the last value,
+/// especially slices that need to allocate.
+///
+/// ```
+/// # use leptos_reactive::*;
+/// # fn really_expensive_computation(value: i32) -> i32 { value };
+/// # let runtime = create_runtime();
+/// pub struct State {
+///     name: String,
+///     token: String,
+/// }
+///
+/// let state = create_rw_signal(State {
+///     name: "Alice".to_owned(),
+///     token: "abcdef".to_owned(),
+/// });
+///
+/// // If we used `create_memo`, we'd need to allocate every time the state changes, but by using
+/// // `create_owning_memo` we can allocate only when `state.name` changes.
+/// let name = create_owning_memo(move |old_name| {
+///     state.with(move |state| {
+///         if let Some(name) =
+///             old_name.filter(|old_name| old_name == &state.name)
+///         {
+///             (name, false)
+///         } else {
+///             (state.name.clone(), true)
+///         }
+///     })
+/// });
+/// let set_name = move |name| state.update(|state| state.name = name);
+///
+/// // We can also re-use the last allocation even when the value changes, which is usually faster,
+/// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
+/// // still be used for the life of the memo).
+/// let token = create_owning_memo(move |old_token| {
+///     state.with(move |state| {
+///         let is_different = old_token.as_ref() != Some(&state.token);
+///         let mut token = old_token.unwrap_or_else(String::new);
+///
+///         if is_different {
+///             token.clone_from(&state.token);
+///         }
+///         (token, is_different)
+///     })
+/// });
+/// let set_token = move |new_token| state.update(|state| state.token = new_token);
+/// # runtime.dispose();
+/// ```
+#[cfg_attr(
+    any(debug_assertions, feature="ssr"),
+    instrument(
+        level = "trace",
+        skip_all,
+        fields(
+            ty = %std::any::type_name::<T>()
+        )
+    )
+)]
+#[track_caller]
+#[inline(always)]
+pub fn create_owning_memo<T>(
+    f: impl Fn(Option<T>) -> (T, bool) + 'static,
+) -> Memo<T>
+where
+    T: PartialEq + 'static,
+{
+    Runtime::current().create_owning_memo(f)
 }
 
 /// An efficient derived reactive value based on other reactive values.
@@ -216,6 +297,65 @@ impl<T> Memo<T> {
     {
         create_memo(f)
     }
+
+    /// Creates a new owning memo from the given function.
+    ///
+    /// This is identical to [`create_owning_memo`].
+    ///
+    /// ```
+    /// # use leptos_reactive::*;
+    /// # fn really_expensive_computation(value: i32) -> i32 { value };
+    /// # let runtime = create_runtime();
+    /// pub struct State {
+    ///     name: String,
+    ///     token: String,
+    /// }
+    ///
+    /// let state = RwSignal::new(State {
+    ///     name: "Alice".to_owned(),
+    ///     token: "abcdef".to_owned(),
+    /// });
+    ///
+    /// // If we used `Memo::new`, we'd need to allocate every time the state changes, but by using
+    /// // `Memo::new_owning` we can allocate only when `state.name` changes.
+    /// let name = Memo::new_owning(move |old_name| {
+    ///     state.with(move |state| {
+    ///         if let Some(name) =
+    ///             old_name.filter(|old_name| old_name == &state.name)
+    ///         {
+    ///             (name, false)
+    ///         } else {
+    ///             (state.name.clone(), true)
+    ///         }
+    ///     })
+    /// });
+    /// let set_name = move |name| state.update(|state| state.name = name);
+    ///
+    /// // We can also re-use the last allocation even when the value changes, which is usually faster,
+    /// // but may have some caveats (e.g. if the value size is drastically reduced, the memory will
+    /// // still be used for the life of the memo).
+    /// let token = Memo::new_owning(move |old_token| {
+    ///     state.with(move |state| {
+    ///         let is_different = old_token.as_ref() != Some(&state.token);
+    ///         let mut token = old_token.unwrap_or_else(String::new);
+    ///
+    ///         if is_different {
+    ///             token.clone_from(&state.token);
+    ///         }
+    ///         (token, is_different)
+    ///     })
+    /// });
+    /// let set_token = move |new_token| state.update(|state| state.token = new_token);
+    /// # runtime.dispose();
+    /// ```
+    #[inline(always)]
+    #[track_caller]
+    pub fn new_owning(f: impl Fn(Option<T>) -> (T, bool) + 'static) -> Memo<T>
+    where
+        T: PartialEq + 'static,
+    {
+        create_owning_memo(f)
+    }
 }
 
 impl<T> Clone for Memo<T>
@@ -524,8 +664,8 @@ impl_get_fn_traits![Memo];
 
 pub(crate) struct MemoState<T, F>
 where
-    T: PartialEq + 'static,
-    F: Fn(Option<&T>) -> T,
+    T: 'static,
+    F: Fn(Option<T>) -> (T, bool),
 {
     pub f: F,
     pub t: PhantomData<T>,
@@ -535,8 +675,8 @@ where
 
 impl<T, F> AnyComputation for MemoState<T, F>
 where
-    T: PartialEq + 'static,
-    F: Fn(Option<&T>) -> T,
+    T: 'static,
+    F: Fn(Option<T>) -> (T, bool),
 {
     #[cfg_attr(
         any(debug_assertions, feature = "ssr"),
@@ -551,24 +691,16 @@ where
         )
     )]
     fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
-        let (new_value, is_different) = {
-            let value = value.borrow();
-            let curr_value = value
-                .downcast_ref::<Option<T>>()
-                .expect("to downcast memo value");
-
-            // run the effect
-            let new_value = (self.f)(curr_value.as_ref());
-            let is_different = curr_value.as_ref() != Some(&new_value);
-            (new_value, is_different)
-        };
-        if is_different {
-            let mut value = value.borrow_mut();
-            let curr_value = value
-                .downcast_mut::<Option<T>>()
-                .expect("to downcast memo value");
-            *curr_value = Some(new_value);
-        }
+        let mut value = value.borrow_mut();
+        let curr_value = value
+            .downcast_mut::<Option<T>>()
+            .expect("to downcast memo value");
+
+        // run the memo
+        let (new_value, is_different) = (self.f)(curr_value.take());
+
+        // set new value
+        *curr_value = Some(new_value);
 
         is_different
     }
diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs
index 61deaf8c..2798fd61 100644
--- a/leptos_reactive/src/runtime.rs
+++ b/leptos_reactive/src/runtime.rs
@@ -1201,12 +1201,12 @@ impl RuntimeId {
 
     #[track_caller]
     #[inline(always)]
-    pub(crate) fn create_memo<T>(
+    pub(crate) fn create_owning_memo<T>(
         self,
-        f: impl Fn(Option<&T>) -> T + 'static,
+        f: impl Fn(Option<T>) -> (T, bool) + 'static,
     ) -> Memo<T>
     where
-        T: PartialEq + Any + 'static,
+        T: 'static,
     {
         Memo {
             id: self.create_concrete_memo(
diff --git a/meta/Cargo.toml b/meta/Cargo.toml
index 22b5d665..2392026e 100644
--- a/meta/Cargo.toml
+++ b/meta/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "leptos_meta"
-version = "0.6.5"
+version = "0.6.6"
 edition = "2021"
 authors = ["Greg Johnston"]
 license = "MIT"
diff --git a/router/Cargo.toml b/router/Cargo.toml
index 581ed123..10014559 100644
--- a/router/Cargo.toml
+++ b/router/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "leptos_router"
-version = "0.6.5"
+version = "0.6.6"
 edition = "2021"
 authors = ["Greg Johnston"]
 license = "MIT"
diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs
index 613987aa..8560d555 100644
--- a/server_fn_macro/src/lib.rs
+++ b/server_fn_macro/src/lib.rs
@@ -119,6 +119,7 @@ pub fn server_macro_impl(
         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(""));
@@ -206,8 +207,11 @@ pub fn server_macro_impl(
         FnArg::Receiver(_) => None,
         FnArg::Typed(t) => Some((&t.pat, &t.ty)),
     });
-    let from_impl =
-        (body.inputs.len() == 1 && first_field.is_some()).then(|| {
+    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! {
@@ -676,6 +680,7 @@ struct ServerFnArgs {
     client: Option<Type>,
     custom_wrapper: Option<Path>,
     builtin_encoding: bool,
+    impl_from: Option<LitBool>,
 }
 
 impl Parse for ServerFnArgs {
@@ -693,6 +698,7 @@ impl Parse for ServerFnArgs {
         let mut res_ty: Option<Type> = None;
         let mut client: Option<Type> = None;
         let mut custom_wrapper: Option<Path> = None;
+        let mut impl_from: Option<LitBool> = None;
 
         let mut use_key_and_value = false;
         let mut arg_pos = 0;
@@ -800,6 +806,14 @@ impl Parse for ServerFnArgs {
                             ));
                         }
                         custom_wrapper = Some(stream.parse()?);
+                    } else if key == "impl_from" {
+                        if impl_from.is_some() {
+                            return Err(syn::Error::new(
+                                key.span(),
+                                "keyword argument repeated: `impl_from`",
+                            ));
+                        }
+                        impl_from = Some(stream.parse()?);
                     } else {
                         return Err(lookahead.error());
                     }
@@ -895,6 +909,7 @@ impl Parse for ServerFnArgs {
             res_ty,
             client,
             custom_wrapper,
+            impl_from,
         })
     }
 }


Test patch:
diff --git a/leptos_reactive/tests/memo.rs b/leptos_reactive/tests/memo.rs
index 35eb8d9a..0d072cd0 100644
--- a/leptos_reactive/tests/memo.rs
+++ b/leptos_reactive/tests/memo.rs
@@ -212,3 +212,84 @@ fn dynamic_dependencies() {
 
     runtime.dispose();
 }
+
+#[test]
+fn owning_memo_slice() {
+    use std::rc::Rc;
+    let runtime = create_runtime();
+
+    // this could be serialized to and from localstorage with miniserde
+    pub struct State {
+        name: String,
+        token: String,
+    }
+
+    let state = create_rw_signal(State {
+        name: "Alice".to_owned(),
+        token: "is this a token????".to_owned(),
+    });
+
+    // We can allocate only when `state.name` changes
+    let name = create_owning_memo(move |old_name| {
+        state.with(move |state| {
+            if let Some(name) =
+                old_name.filter(|old_name| old_name == &state.name)
+            {
+                (name, false)
+            } else {
+                (state.name.clone(), true)
+            }
+        })
+    });
+    let set_name = move |name| state.update(|state| state.name = name);
+
+    // We can also re-use the last token allocation, which may be even better if the tokens are
+    // always of the same length
+    let token = create_owning_memo(move |old_token| {
+        state.with(move |state| {
+            let is_different = old_token.as_ref() != Some(&state.token);
+            let mut token = old_token.unwrap_or_else(String::new);
+
+            if is_different {
+                token.clone_from(&state.token);
+            }
+            (token, is_different)
+        })
+    });
+    let set_token =
+        move |new_token| state.update(|state| state.token = new_token);
+
+    let count_name_updates = Rc::new(std::cell::Cell::new(0));
+    assert_eq!(count_name_updates.get(), 0);
+    create_isomorphic_effect({
+        let count_name_updates = Rc::clone(&count_name_updates);
+        move |_| {
+            name.track();
+            count_name_updates.set(count_name_updates.get() + 1);
+        }
+    });
+    assert_eq!(count_name_updates.get(), 1);
+
+    let count_token_updates = Rc::new(std::cell::Cell::new(0));
+    assert_eq!(count_token_updates.get(), 0);
+    create_isomorphic_effect({
+        let count_token_updates = Rc::clone(&count_token_updates);
+        move |_| {
+            token.track();
+            count_token_updates.set(count_token_updates.get() + 1);
+        }
+    });
+    assert_eq!(count_token_updates.get(), 1);
+
+    set_name("Bob".to_owned());
+    name.with(|name| assert_eq!(name, "Bob"));
+    assert_eq!(count_name_updates.get(), 2);
+    assert_eq!(count_token_updates.get(), 1);
+
+    set_token("this is not a token!".to_owned());
+    token.with(|token| assert_eq!(token, "this is not a token!"));
+    assert_eq!(count_name_updates.get(), 2);
+    assert_eq!(count_token_updates.get(), 2);
+
+    runtime.dispose();
+}