feat: create reactive traits (AsReactive and ReactiveCapture)

This commit is contained in:
alphafetus committed 2025-09-03 11:28:59 -04:00
1 parent d01b568f15
commit ec26fb6fce
9 files changed
+209 -28

No files matched your search

+3 -1
View File
@@ -8,4 +8,6 @@ proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
quote = "1.0"
traits = { path = "../traits" }
+81 -10
View File
@@ -1,7 +1,72 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, Ident, parse_macro_input};
use syn::{parse_macro_input, Data, Ident};
/// Derives the `AsReactive` and `ReactiveCapture` traits for a struct, while also
/// providing a new reactive struct with the same field names, but they are wrapped
/// in the Leptos `RwSignal`.
///
/// Fields that are custom types are not automatically converted into reactive types. They
/// will be wrapped in an `RwSignal` struct, but that is where this process stops. Making
/// other information reactive must be implemented manually.
///
/// For example, when the `Reactive` macro is expanded for the struct `User`, it will look
/// like this:
///
/// ```
/// #[derive(Reactive)]
/// struct User {
/// id: i32,
/// name: String,
/// custom: CustomType
/// }
///
/// // This code will be added:
/// struct UserReactive {
/// id: RwSignal<i32>,
/// name: RwSignal<String>,
/// custom: RwSignal<CustomType>
/// }
///
/// impl AsReactive for User {
/// type ReactiveType = UserReactive;
/// // Other methods - refer to AsReactive documentation.
/// }
///
/// impl ReactiveCapture for UserReactive {
/// type CaptureType = User;
/// // Other methods - refer to ReactiveCapture documentation.
/// }
/// ```
///
/// The base type (`User`) can then be converted into the reactive type:
/// ```
/// let user = User::new();
/// let user_reactive = user.as_reactive();
/// ```
/// And captured back into a non-reactive type:
/// ```
/// let captured_user = user_reactive.capture()
/// ```
///
/// # Important Considerations
///
/// A reminder that custom types, in this case `CustomType`, are just wrapped in an `RwSignal`.
/// They are not automatically converted to their own `Reactive` type in the case that they
/// derive this macro. As a result, custom reactive types *must* be updated manually by utilizing
/// the `RwSignal::update` and `RwSignal::set` methods.
///
/// For example, the `UserReactive` struct from earlier does not convert the `CustomType` into a
/// `CustomTypeReactive`. We have to do that ourselves:
/// ```
/// let user: User = User::new();
/// let user_reactive: UserReactive = user.as_reactive();
/// let custom_reactive: CustomTypeReactive = user_reactive.custom.as_reactive();
/// // And then convert back...
/// user_reactive.custom.set(custom_reactive.capture());
/// ```
/// If a shorthand for the `capture` and `as_reactive` functions are absolutely *required*, then this
/// macro is probably not the way to go - as of now, this functionality should be impmlemented manually.
#[proc_macro_derive(Reactive)]
pub fn make_reactive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
@@ -32,7 +97,7 @@ pub fn make_reactive(input: TokenStream) -> TokenStream {
// Create new function arguments/inits
let new_fn_arg = quote! {
#field_name: _RwSignal::new(data.#field_name),
#field_name: _RwSignal::new(self.#field_name),
};
let from_fn_arg = quote! {
@@ -43,30 +108,36 @@ pub fn make_reactive(input: TokenStream) -> TokenStream {
from_fn_args.push(from_fn_arg);
}
let reactive_docs = format!("Reactive version of the [`{name}`] struct. Automatically created by the `Reactive` macro.");
let reactive_docs = format!(
"Reactive version of the [`{name}`] struct. Automatically created by the `Reactive` macro."
);
// Expand the macro. The output is a struct with new reactive fields and a new function,
// which initializes the reactive fields based on the original struct's fields.
let expanded = quote! {
use leptos::prelude::RwSignal as _RwSignal; // in case the user imports it themselves
use leptos::prelude::GetUntracked as _;
use traits::{AsReactive, ReactiveCapture};
#[doc = #reactive_docs]
pub struct #reactive_name {
#(#new_fields)*
}
impl #reactive_name {
/// Creates a new `#reactive_name` struct from a non-reactive `#name`.
pub fn new(data: #name) -> Self {
Self {
impl AsReactive for #name {
type ReactiveType = #reactive_name;
fn as_reactive(self) -> Self::ReactiveType {
#reactive_name {
#(#new_fn_args)*
}
}
}
/// Captures the information of this struct into a `#name` struct, for use
/// in server functions.
pub fn capture(&self) -> #name {
impl ReactiveCapture for #reactive_name {
type CaptureType = #name;
fn capture(&self) -> Self::CaptureType {
#name {
#(#from_fn_args)*
}