From 66d7cb5d12d2522afb2345751ec79f4be791dfd7 Mon Sep 17 00:00:00 2001 From: Saber Haj Rabiee Date: Wed, 14 Aug 2024 06:14:17 -0700 Subject: [PATCH] feat: add `memo!` macro (closes #2708) (#2826) --- leptos_macro/src/lib.rs | 33 ++++++++++++++++++ leptos_macro/src/memo.rs | 56 ++++++++++++++++++++++++++++++ leptos_macro/tests/memo.rs | 33 ++++++++++++++++++ leptos_macro/tests/memo/red.rs | 26 ++++++++++++++ leptos_macro/tests/memo/red.stderr | 31 +++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 leptos_macro/src/memo.rs create mode 100644 leptos_macro/tests/memo.rs create mode 100644 leptos_macro/tests/memo/red.rs create mode 100644 leptos_macro/tests/memo/red.stderr diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index 3bb5a2c6d..786ca73d8 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -19,6 +19,7 @@ mod params; mod view; use crate::component::unmodified_fn_name_from_fn_name; mod component; +mod memo; mod slice; mod slot; @@ -875,3 +876,35 @@ pub fn params_derive( pub fn slice(input: TokenStream) -> TokenStream { slice::slice_impl(input) } + +/// Generates a `memo` into a struct with a default getter. +/// +/// Can be used to access deeply nested fields within a global state object. +/// +/// ```rust +/// # use leptos::prelude::*; +/// # use leptos_macro::memo; +/// +/// #[derive(Default)] +/// pub struct Outer { +/// count: i32, +/// inner: Inner, +/// } +/// +/// #[derive(Default)] +/// pub struct Inner { +/// inner_count: i32, +/// inner_name: String, +/// } +/// +/// let outer_signal = RwSignal::new(Outer::default()); +/// +/// let count = memo!(outer_signal.count); +/// +/// let inner_count = memo!(outer_signal.inner.inner_count); +/// let inner_name = memo!(outer_signal.inner.inner_name); +/// ``` +#[proc_macro] +pub fn memo(input: TokenStream) -> TokenStream { + memo::memo_impl(input) +} diff --git a/leptos_macro/src/memo.rs b/leptos_macro/src/memo.rs new file mode 100644 index 000000000..9e2c5fcf1 --- /dev/null +++ b/leptos_macro/src/memo.rs @@ -0,0 +1,56 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::{quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + Token, +}; + +struct MemoMacroInput { + root: syn::Ident, + path: Punctuated, +} + +impl Parse for MemoMacroInput { + fn parse(input: ParseStream) -> syn::Result { + let root: syn::Ident = input.parse()?; + input.parse::()?; + // do not accept trailing punctuation + let path: Punctuated = + Punctuated::parse_separated_nonempty(input)?; + + if path.is_empty() { + return Err(input.error("expected identifier")); + } + + if !input.is_empty() { + return Err(input.error("unexpected token")); + } + + Ok(Self { root, path }) + } +} + +impl ToTokens for MemoMacroInput { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let root = &self.root; + let path = &self.path; + + tokens.extend(quote! { + ::leptos::reactive_graph::computed::Memo::new( + move |_| { + use ::leptos::reactive_graph::traits::With; + #root.with(|st: _| st.#path.clone()) + } + ) + }) + } +} + +pub fn memo_impl(tokens: TokenStream) -> TokenStream { + let input = parse_macro_input!(tokens as MemoMacroInput); + input.into_token_stream().into() +} diff --git a/leptos_macro/tests/memo.rs b/leptos_macro/tests/memo.rs new file mode 100644 index 000000000..273f7e1bb --- /dev/null +++ b/leptos_macro/tests/memo.rs @@ -0,0 +1,33 @@ +use leptos::prelude::RwSignal; +use leptos_macro::memo; + +#[derive(Default)] +pub struct OuterState { + count: i32, + inner: InnerState, +} + +#[derive(Clone, PartialEq, Default)] +pub struct InnerState { + inner_count: i32, + inner_tuple: InnerTuple, +} + +#[derive(Clone, PartialEq, Default)] +pub struct InnerTuple(String); + +#[test] +fn green() { + let outer_signal = RwSignal::new(OuterState::default()); + + let _ = memo!(outer_signal.count); + + let _ = memo!(outer_signal.inner.inner_count); + let _ = memo!(outer_signal.inner.inner_tuple.0); +} + +#[test] +fn red() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/memo/red.rs") +} diff --git a/leptos_macro/tests/memo/red.rs b/leptos_macro/tests/memo/red.rs new file mode 100644 index 000000000..971333d03 --- /dev/null +++ b/leptos_macro/tests/memo/red.rs @@ -0,0 +1,26 @@ +use leptos::prelude::RwSignal; +use leptos_macro::memo; + +#[derive(Default, PartialEq)] +pub struct OuterState { + count: i32, + inner: InnerState, +} + +#[derive(Clone, PartialEq, Default)] +pub struct InnerState { + inner_count: i32, + inner_name: String, +} + +fn main() { + let outer_signal = RwSignal::new(OuterState::default()); + + let _ = memo!(); + + let _ = memo!(outer_signal); + + let _ = memo!(outer_signal.); + + let _ = memo!(outer_signal.inner.); +} diff --git a/leptos_macro/tests/memo/red.stderr b/leptos_macro/tests/memo/red.stderr new file mode 100644 index 000000000..f99722f8e --- /dev/null +++ b/leptos_macro/tests/memo/red.stderr @@ -0,0 +1,31 @@ +error: unexpected end of input, expected identifier + --> tests/memo/red.rs:19:13 + | +19 | let _ = memo!(); + | ^^^^^^^ + | + = note: this error originates in the macro `memo` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected `.` + --> tests/memo/red.rs:21:13 + | +21 | let _ = memo!(outer_signal); + | ^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `memo` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: unexpected end of input, expected identifier or integer + --> tests/memo/red.rs:23:13 + | +23 | let _ = memo!(outer_signal.); + | ^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `memo` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: unexpected end of input, expected identifier or integer + --> tests/memo/red.rs:25:13 + | +25 | let _ = memo!(outer_signal.inner.); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `memo` (in Nightly builds, run with -Z macro-backtrace for more info)