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

Generated
+9
View File
@@ -2159,6 +2159,7 @@ version = "0.1.0"
dependencies = [
"quote",
"syn",
"traits",
]
[[package]]
@@ -2960,6 +2961,7 @@ dependencies = [
"serde",
"serde_dynamo",
"tokio",
"traits",
"url",
"uuid",
"wasm-bindgen",
@@ -3645,6 +3647,13 @@ dependencies = [
"once_cell",
]
[[package]]
name = "traits"
version = "0.1.0"
dependencies = [
"leptos",
]
[[package]]
name = "try-lock"
version = "0.2.5"
+2 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "scholarships-rs"
version = "0.1.0"
edition = "2021"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
@@ -24,6 +24,7 @@ leptos_oidc = "0.9.0"
url = "2.5.4"
macros = { path = "./macros" }
traits = { path = "./traits" }
[features]
hydrate = [
+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)*
}
+19 -14
View File
@@ -2,10 +2,11 @@ use std::collections::HashMap;
use crate::components::ActionButton;
use leptos::prelude::*;
use macros::Reactive;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Clone, Serialize, Deserialize, Debug)]
#[derive(Clone, Serialize, Deserialize, Debug, Reactive, Default)]
pub struct MultiEntryData {
pub id: Uuid,
pub data: HashMap<String, ValueType>,
@@ -130,27 +131,31 @@ pub fn MultiEntry(
<div class="rounded-md bg-red-700">
// This div MUST have the same spacing CSS as the main entry div
<div class="flex bg-inherit p-3 m-1">
<span class="flex-1 text-white font-bold">{name_member.get().display_name}</span>
<span class="flex-1 text-white font-bold">{info_member.get().display_name}</span>
<span class="flex-1 text-white font-bold">
{name_member.get().display_name}
</span>
<span class="flex-1 text-white font-bold">
{info_member.get().display_name}
</span>
</div>
</div>
// Entry section
<div>
<For
each = move || entries.get()
key = |entry: &MultiEntryData| entry.id
children = move |entry| view! {
<Entry
entry_data=entry.clone()
name_member=name_member
info_member=info_member
/>
each=move || entries.get()
key=|entry: &MultiEntryData| entry.id
children=move |entry| {
view! {
<Entry
entry_data=entry.clone()
name_member=name_member
info_member=info_member
/>
}
}
/>
</div>
</div>
<ActionButton on:click=create_entry>
Add Entry
</ActionButton>
<ActionButton on:click=create_entry>Add Entry</ActionButton>
}
}
+3 -2
View File
@@ -6,7 +6,7 @@ use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue, Clien
use serde_dynamo::{from_item, to_item};
use crate::app::Unauthenticated;
use crate::common::{StudentInfo, StudentInfoReactive, UserClaims};
use crate::common::{StudentInfo, UserClaims};
use crate::components::{
ActionButton, CheckboxList, Loading, OutlinedTextField, Panel, Row, Select,
RadioList
@@ -14,6 +14,7 @@ use crate::components::{
use leptos::leptos_dom::logging::console_log;
use leptos::prelude::*;
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
use traits::{AsReactive, ReactiveCapture};
#[server(GetSubmission, endpoint = "/get-submission")]
pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
@@ -119,7 +120,7 @@ pub fn HomePage() -> impl IntoView {
server_resource
.get()
.map(|submission: StudentInfo| {
let reactive_info = StudentInfoReactive::new(submission);
let reactive_info = submission.as_reactive();
let select_value = RwSignal::new(String::from("Math"));
let chk_select = RwSignal::new(vec!["Testing 2".into()]);
let elements_disabled = RwSignal::new(false);
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "traits"
version = "0.1.0"
edition = "2024"
[dependencies]
leptos = { version = "0.8" }
+3
View File
@@ -0,0 +1,3 @@
pub mod reactive;
pub use reactive::*;
+82
View File
@@ -0,0 +1,82 @@
/// Trait that ensures a reactive struct can be created from a non-reactive struct.
///
/// For example:
///
/// ```rust
/// struct User {
/// id: i32,
/// name: String
/// }
///
/// struct UserReactive {
/// id: RwSignal<i32>,
/// name: RwSignal<String>
/// }
///
/// impl ReactiveCapture for UserReactive {
/// // implement - required for ReactiveType.
/// }
///
/// impl AsReactive for User {
/// type ReactiveType = UserReactive;
/// fn as_reactive(self) -> UserReactive {
/// UserReactive {
/// id: RwSignal::new(self.id),
/// name: RwSignal::new(self.name)
/// }
/// }
/// }
/// ```
pub trait AsReactive {
/// The type of the reactive struct. It must implement `ReactiveCapture`.
type ReactiveType: ReactiveCapture;
/// Wraps all fields of the struct into an `RwSignal`, returning a new
/// "reactive" struct.
///
/// Note that this method consumes the original struct and returns a new
/// reactive struct.
fn as_reactive(self) -> Self::ReactiveType;
}
/// Trait that ensures a reactive struct can be captured into a non-reactive struct.
/// The "captured" struct does not contain *any* reactive fields, meaning that it is not
/// wrapped in an `RwSignal`.
///
/// The captured struct should simply collect all the values from the reactive fields at
/// that point in time.
///
/// For example:
///
/// ```rust
/// struct User {
/// id: i32,
/// name: String
/// }
///
/// struct UserReactive {
/// id: RwSignal<i32>,
/// name: RwSignal<String>
/// }
///
/// impl AsReactive for User {
/// // implement - required for CaptureType.
/// }
///
/// impl ReactiveCapture for UserReactive {
/// type CaptureType = User;
/// fn capture(self) -> User {
/// User {
/// id: self.id.get(),
/// name: self.name.get()
/// }
/// }
/// }
/// ```
pub trait ReactiveCapture {
/// The type of the captured struct. It must implement `AsReactive`.
type CaptureType: AsReactive;
/// Captures the current state of the reactive struct into a non-reactive struct.
fn capture(&self) -> Self::CaptureType;
}