1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
use darling::{ast::Data, FromDeriveInput, FromField, ToTokens};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{Attribute, Ident, Type, Visibility};
/// This is used to parse what the user gives us with `darling`.
#[derive(Debug, FromDeriveInput)]
#[darling(attributes(rx))]
pub struct ReactiveStateDeriveInput {
/// If specified, a type alias will be created for the final reactive
/// `struct` for ease of reference.
#[darling(default)]
alias: Option<Ident>,
/// If specified, the type should be ignored by HSR.
#[darling(default)]
hsr_ignore: bool,
ident: Ident,
vis: Visibility,
// The first of these is only relevant if we're parsing `enum`s, which we aren't
pub data: Data<darling::util::Ignored, ReactiveStateField>,
attrs: Vec<Attribute>,
}
/// This is used to parse each individual field in what the user gives us.
#[derive(Debug, FromField, Clone)]
#[darling(attributes(rx))]
pub struct ReactiveStateField {
/// Whether or not we should expect the annotated field to be able to made
/// reactive itself, enabling nested reactivity.
#[darling(default)]
nested: bool,
/// This is used to mark fields that have a browser-side handler dedicated
/// to modifying their value asynchronously. When the page is loaded, the
/// provided modifier function will be called with an `RcSignal` of this
/// field's type (even if this is used with `#[rx(nested)]`!).
///
/// The reason handlers are only allowed to work with individual fields is
/// to enable fine-grained error handling, by forcing users to handle
/// the possibility that each of their handlers comes up with an error.
///
/// Note that the handler function specified must be asynchronous, but it
/// will be placed in an abortable future: when the user leaves this
/// page, any ongoing handlers will be *immmediately* short-circuited.
/// (You shouldn't have to worry about this unless you're doing
/// something very advanced.)
#[darling(default)]
suspense: Option<Ident>,
ident: Option<Ident>,
vis: Visibility,
ty: Type,
attrs: Vec<syn::Attribute>,
}
/// The underlying implementation of the `ReactiveState` derive macro, which
/// implements the traits involved in Perseus' reactive state platform, creating
/// an intermediary reactive struct using `RcSignal`s and a final one using
/// `&'cx Signal`s, where `cx` is a Sycamore scope lifetime.
pub fn make_rx_impl(input: ReactiveStateDeriveInput) -> TokenStream {
// Extract the fields of the `struct`
let fields = match input.data {
Data::Struct(fields) => fields.fields,
Data::Enum(_) => return syn::Error::new_spanned(
input.ident,
"you can only make `struct`s reactive with this macro (`enum` capability will be added in a future release, for now you'll have to implement it manually)"
).to_compile_error(),
};
// Now go through them and create what we want for both the intermediate and the
// reactive `struct`s
let mut intermediate_fields = quote!();
let mut intermediate_field_makers = quote!();
let mut new_intermediate_field_makers = quote!();
let mut unrx_field_makers = quote!();
let mut suspense_commands = quote!();
let mut old_types = quote!();
for field in fields.iter() {
let old_ty = field.ty.to_token_stream();
let field_ident = field.ident.as_ref().unwrap(); // It's a `struct`, so this is defined
let field_vis = &field.vis;
let mut field_attrs = quote!();
for attr in field.attrs.iter() {
field_attrs.extend(attr.to_token_stream());
}
// Old for ::new implementation of intermediate type
old_types.extend(quote! {
#field_ident: #old_ty,
});
// Nested fields are left as-is, non-nested ones are wrapped in `RcSignal`s
if field.nested {
// Nested types should implement the necessary linking traits
intermediate_fields.extend(quote! {
#field_attrs
#field_vis #field_ident: <#old_ty as ::perseus::state::MakeRx>::Rx,
});
intermediate_field_makers.extend(quote! { #field_ident: self.#field_ident.make_rx(), });
new_intermediate_field_makers.extend(quote! { #field_ident: #field_ident.make_rx(), });
unrx_field_makers
.extend(quote! { #field_ident: self.#field_ident.clone().make_unrx(), });
// Handle suspended fields
if let Some(handler) = &field.suspense {
// This line calls a utility function that does ergonomic error handling
suspense_commands.extend(quote! {
// The `nested` part makes this expect `RxResult`
::perseus::state::compute_nested_suspense(
cx,
self.#field_ident.clone(),
#handler(
cx,
::sycamore::prelude::create_ref(cx, self.#field_ident.clone()),
),
);
});
} else {
// If this field is not suspended, it might have suspended children, which we
// should be sure to compute
suspense_commands.extend(quote! {
self.#field_ident.compute_suspense(cx);
})
}
} else {
intermediate_fields.extend(quote! {
#field_attrs
#field_vis #field_ident: ::sycamore::prelude::RcSignal<#old_ty>,
});
intermediate_field_makers.extend(
quote! { #field_ident: ::sycamore::prelude::create_rc_signal(self.#field_ident), },
);
new_intermediate_field_makers.extend(
quote! { #field_ident: ::sycamore::prelude::create_rc_signal(#field_ident), },
);
// All fields must be `Clone`
unrx_field_makers
.extend(quote! { #field_ident: (*self.#field_ident.get_untracked()).clone(), });
// Handle suspended fields (we don't care if they're nested, the user can worry
// about that (probably using `RxResult` or similar))
if let Some(handler) = &field.suspense {
// This line calls a utility function that does ergonomic error handling
suspense_commands.extend(quote! {
// The `nested` part makes this expect `RxResult`
::perseus::state::compute_suspense(
cx,
self.#field_ident.clone(),
#handler(
cx,
::sycamore::prelude::create_ref(cx, self.#field_ident.clone()),
),
);
});
}
}
}
let ReactiveStateDeriveInput {
ident,
vis,
attrs: attrs_vec,
alias,
hsr_ignore,
..
} = input;
let mut attrs = quote!();
for attr in attrs_vec.iter() {
attrs.extend(attr.to_token_stream());
}
let intermediate_ident = Ident::new(
&(ident.to_string() + "PerseusRxIntermediate"),
Span::call_site(),
);
// Create a type alias for the final reactive version for convenience, if the
// user asked for one
let ref_alias = if let Some(alias) = alias {
// // We use the full form for a cleaner expansion in IDEs
// quote! { #vis type #alias<'__derived_rx> = <<#ident as
// ::perseus::state::MakeRx>::Rx as
// ::perseus::state::MakeRxRef>::RxRef<'__derived_rx>; }
quote! { #vis type #alias = #intermediate_ident; }
} else {
quote!()
};
// TODO Generics support
quote! {
#attrs
#[derive(Clone)]
#vis struct #intermediate_ident {
#intermediate_fields
}
impl From<#intermediate_ident> for #ident
{
fn from(value: #intermediate_ident) -> #ident
{
use ::perseus::state::MakeUnrx;
value.make_unrx()
}
}
impl From<#ident> for #intermediate_ident
{
fn from(value: #ident) -> #intermediate_ident
{
use ::perseus::state::MakeRx;
value.make_rx()
}
}
impl ::perseus::state::MakeRx for #ident {
type Rx = #intermediate_ident;
#[cfg(debug_assertions)]
const HSR_IGNORE: bool = #hsr_ignore;
fn make_rx(self) -> Self::Rx {
use ::perseus::state::MakeRx;
Self::Rx {
#intermediate_field_makers
}
}
}
impl ::perseus::state::MakeUnrx for #intermediate_ident {
type Unrx = #ident;
fn make_unrx(self) -> Self::Unrx {
use ::perseus::state::MakeUnrx;
Self::Unrx {
#unrx_field_makers
}
}
#[cfg(client)]
fn compute_suspense<'a>(&self, cx: ::sycamore::prelude::Scope<'a>) {
#suspense_commands
}
}
impl ::perseus::state::Freeze for #intermediate_ident {
fn freeze(&self) -> ::std::string::String {
use ::perseus::state::MakeUnrx;
let unrx = self.clone().make_unrx();
// TODO Is this `.unwrap()` safe?
::serde_json::to_string(&unrx).unwrap()
}
}
#ref_alias
}
}