Merge pull request #2 from R15-Software-Dev/feature/macro-requirements
Adds new `AsReactive` and `ReactiveCapture` traits, and automatically implements them through the altered `Reactive` derive macro.
This commit was merged in pull request #2.
This commit is contained in:
9 files changed
+213
-28
No files matched your search
Generated
+9
@@ -2159,6 +2159,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
|
"traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2960,6 +2961,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_dynamo",
|
"serde_dynamo",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"traits",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -3645,6 +3647,13 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "traits"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "scholarships-rs"
|
name = "scholarships-rs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
@@ -24,6 +24,7 @@ leptos_oidc = "0.9.0"
|
|||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
|
|
||||||
macros = { path = "./macros" }
|
macros = { path = "./macros" }
|
||||||
|
traits = { path = "./traits" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
hydrate = [
|
hydrate = [
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ proc-macro = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
syn = "2.0"
|
syn = "2.0"
|
||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
|
|
||||||
|
traits = { path = "../traits" }
|
||||||
+81
-10
@@ -1,7 +1,72 @@
|
|||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use quote::quote;
|
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.get().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 implemented manually.
|
||||||
#[proc_macro_derive(Reactive)]
|
#[proc_macro_derive(Reactive)]
|
||||||
pub fn make_reactive(input: TokenStream) -> TokenStream {
|
pub fn make_reactive(input: TokenStream) -> TokenStream {
|
||||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
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
|
// Create new function arguments/inits
|
||||||
let new_fn_arg = quote! {
|
let new_fn_arg = quote! {
|
||||||
#field_name: _RwSignal::new(data.#field_name),
|
#field_name: _RwSignal::new(self.#field_name),
|
||||||
};
|
};
|
||||||
|
|
||||||
let from_fn_arg = quote! {
|
let from_fn_arg = quote! {
|
||||||
@@ -43,30 +108,36 @@ pub fn make_reactive(input: TokenStream) -> TokenStream {
|
|||||||
from_fn_args.push(from_fn_arg);
|
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,
|
// 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.
|
// which initializes the reactive fields based on the original struct's fields.
|
||||||
let expanded = quote! {
|
let expanded = quote! {
|
||||||
use leptos::prelude::RwSignal as _RwSignal; // in case the user imports it themselves
|
use leptos::prelude::RwSignal as _RwSignal; // in case the user imports it themselves
|
||||||
use leptos::prelude::GetUntracked as _;
|
use leptos::prelude::GetUntracked as _;
|
||||||
|
use traits::{AsReactive, ReactiveCapture};
|
||||||
|
|
||||||
#[doc = #reactive_docs]
|
#[doc = #reactive_docs]
|
||||||
pub struct #reactive_name {
|
pub struct #reactive_name {
|
||||||
#(#new_fields)*
|
#(#new_fields)*
|
||||||
}
|
}
|
||||||
|
|
||||||
impl #reactive_name {
|
impl AsReactive for #name {
|
||||||
/// Creates a new `#reactive_name` struct from a non-reactive `#name`.
|
type ReactiveType = #reactive_name;
|
||||||
pub fn new(data: #name) -> Self {
|
|
||||||
Self {
|
fn as_reactive(self) -> Self::ReactiveType {
|
||||||
|
#reactive_name {
|
||||||
#(#new_fn_args)*
|
#(#new_fn_args)*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Captures the information of this struct into a `#name` struct, for use
|
impl ReactiveCapture for #reactive_name {
|
||||||
/// in server functions.
|
type CaptureType = #name;
|
||||||
pub fn capture(&self) -> #name {
|
|
||||||
|
fn capture(&self) -> Self::CaptureType {
|
||||||
#name {
|
#name {
|
||||||
#(#from_fn_args)*
|
#(#from_fn_args)*
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use crate::components::ActionButton;
|
use crate::components::ActionButton;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use macros::Reactive;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
#[derive(Clone, Serialize, Deserialize, Debug, Reactive, Default)]
|
||||||
pub struct MultiEntryData {
|
pub struct MultiEntryData {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub data: HashMap<String, ValueType>,
|
pub data: HashMap<String, ValueType>,
|
||||||
@@ -130,27 +131,31 @@ pub fn MultiEntry(
|
|||||||
<div class="rounded-md bg-red-700">
|
<div class="rounded-md bg-red-700">
|
||||||
// This div MUST have the same spacing CSS as the main entry div
|
// This div MUST have the same spacing CSS as the main entry div
|
||||||
<div class="flex bg-inherit p-3 m-1">
|
<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">
|
||||||
<span class="flex-1 text-white font-bold">{info_member.get().display_name}</span>
|
{name_member.get().display_name}
|
||||||
|
</span>
|
||||||
|
<span class="flex-1 text-white font-bold">
|
||||||
|
{info_member.get().display_name}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
// Entry section
|
// Entry section
|
||||||
<div>
|
<div>
|
||||||
<For
|
<For
|
||||||
each = move || entries.get()
|
each=move || entries.get()
|
||||||
key = |entry: &MultiEntryData| entry.id
|
key=|entry: &MultiEntryData| entry.id
|
||||||
children = move |entry| view! {
|
children=move |entry| {
|
||||||
<Entry
|
view! {
|
||||||
entry_data=entry.clone()
|
<Entry
|
||||||
name_member=name_member
|
entry_data=entry.clone()
|
||||||
info_member=info_member
|
name_member=name_member
|
||||||
/>
|
info_member=info_member
|
||||||
|
/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ActionButton on:click=create_entry>
|
<ActionButton on:click=create_entry>Add Entry</ActionButton>
|
||||||
Add Entry
|
|
||||||
</ActionButton>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue, Clien
|
|||||||
use serde_dynamo::{from_item, to_item};
|
use serde_dynamo::{from_item, to_item};
|
||||||
|
|
||||||
use crate::app::Unauthenticated;
|
use crate::app::Unauthenticated;
|
||||||
use crate::common::{StudentInfo, StudentInfoReactive, UserClaims};
|
use crate::common::{StudentInfo, UserClaims};
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
ActionButton, CheckboxList, Loading, OutlinedTextField, Panel, Row, Select,
|
ActionButton, CheckboxList, Loading, OutlinedTextField, Panel, Row, Select,
|
||||||
RadioList
|
RadioList
|
||||||
@@ -14,14 +14,18 @@ use crate::components::{
|
|||||||
use leptos::leptos_dom::logging::console_log;
|
use leptos::leptos_dom::logging::console_log;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
|
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
|
||||||
|
use traits::{AsReactive, ReactiveCapture};
|
||||||
|
|
||||||
#[server(GetSubmission, endpoint = "/get-submission")]
|
#[server(GetSubmission, endpoint = "/get-submission")]
|
||||||
pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
|
pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
|
||||||
|
|
||||||
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
||||||
let dbclient = Client::new(&config);
|
let dbclient = Client::new(&config);
|
||||||
|
|
||||||
console_log(format!("Getting values from API using username {}", id).as_str());
|
console_log(format!("Getting values from API using username {}", id).as_str());
|
||||||
|
|
||||||
|
// Gets the item from the database. It only returns an error if the database
|
||||||
|
// experiences an error - not if the item is not found.
|
||||||
match dbclient
|
match dbclient
|
||||||
.get_item()
|
.get_item()
|
||||||
.table_name("student-applications")
|
.table_name("student-applications")
|
||||||
@@ -51,7 +55,6 @@ pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
|
|||||||
pub async fn create_sample_submission(
|
pub async fn create_sample_submission(
|
||||||
student_info: StudentInfo,
|
student_info: StudentInfo,
|
||||||
) -> Result<(), ServerFnError> {
|
) -> Result<(), ServerFnError> {
|
||||||
use aws_sdk_dynamodb::Client;
|
|
||||||
|
|
||||||
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
||||||
let dbclient = Client::new(&config);
|
let dbclient = Client::new(&config);
|
||||||
@@ -68,10 +71,12 @@ pub async fn create_sample_submission(
|
|||||||
.send().await
|
.send().await
|
||||||
{
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
// The student's information was successfully created
|
||||||
console_log(format!("Set up student information: {:?}", student_info).as_str());
|
console_log(format!("Set up student information: {:?}", student_info).as_str());
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
// There was an error while creating the student's information
|
||||||
let msg = err.message().unwrap_or("An unknown error occurred.");
|
let msg = err.message().unwrap_or("An unknown error occurred.");
|
||||||
console_log(format!("Error creating sample submission: {}", msg).as_str());
|
console_log(format!("Error creating sample submission: {}", msg).as_str());
|
||||||
Err(ServerFnError::new(msg))
|
Err(ServerFnError::new(msg))
|
||||||
@@ -119,7 +124,7 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
server_resource
|
server_resource
|
||||||
.get()
|
.get()
|
||||||
.map(|submission: StudentInfo| {
|
.map(|submission: StudentInfo| {
|
||||||
let reactive_info = StudentInfoReactive::new(submission);
|
let reactive_info = submission.as_reactive();
|
||||||
let select_value = RwSignal::new(String::from("Math"));
|
let select_value = RwSignal::new(String::from("Math"));
|
||||||
let chk_select = RwSignal::new(vec!["Testing 2".into()]);
|
let chk_select = RwSignal::new(vec!["Testing 2".into()]);
|
||||||
let elements_disabled = RwSignal::new(false);
|
let elements_disabled = RwSignal::new(false);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "traits"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
leptos = { version = "0.8" }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod reactive;
|
||||||
|
|
||||||
|
pub use reactive::*;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user