dynamic_struct/lib.rs
1//! A derive macro for creating **push-based** reactive properties for structs (with named fields only).
2//!
3//! # Why push-based?
4//! Lazy *poll-based* reactive systems typically require wrapping the values and adding RefCells or flags to cache and update values. Event-based system require a subscription model.
5//!
6//! The plumbing for adding *push-based* change propagation is done via macros at compile-time and the generated code can be inlined during compilation, becoming a zero-cost abstraction at run-time (same as re-calculating the dynamic properties by hand when their dependencies change)
7//!
8//! The types can also be left untouched, no need for wrapping and dereferencing.
9//!
10//! # How to use
11//! 1. Add as a dependency to the Cargo file
12//! ```toml
13//! [dependencies]
14//! dynamic-struct = "*"
15//! ```
16//!
17//! 2. Add the derive macro to the struct and mark the properties that are dynamic
18//! ```ignore
19//! use dynamic_struct::Dynamic;
20//!
21//! #[derive(Dynamic)]
22//! struct Demo {
23//! a: u32,
24//! b: u32,
25//! #[dynamic((a, b), calculate_c)]
26//! c: u32,
27//! }
28//!
29//! impl Demo {
30//! fn calculate_c(&mut self) {
31//! self.c = self.a + self.b
32//! }
33//! }
34//! ```
35//!
36//! The attribute for the properties has the following structure:
37//! ```ignore
38//! #[dynamic(tuple of dependent property names, name of local method name)]
39//! ```
40//!
41//! The local method must have the call signature matching `fn name(&mut self)`.
42//!
43//! 3. Update the properties using the generated mutate functions
44//! ```ignore
45//! let demo = Demo { a: 1, b: 2, c: 3 };
46//!
47//! dbg!(demo.c); //3
48//! demo.update_a(7);
49//! dbg!(demo.c); //9
50//! ```
51//!
52//! # How it works
53//!
54//! 1. Functions are created to signal when a property is changed, it is populated with the methods that should be called.
55//!
56//! ```ignore
57//! impl Demo {
58//! #[inline]
59//! pub fn updated_a(&mut self) {
60//! self.update_c();
61//! }
62//! }
63//! ```
64//!
65//! Note: properties that do not propagate changes will still be created but will be empty.
66//!
67//! 2. Functions are created for each property to update the property
68//!
69//! For **non-dynamic** properties, the value can be set via a parameter matching the field type, then the field updated function is called (listed above).
70//!
71//! ```ignore
72//! impl Demo {
73//! #[inline]
74//! pub fn update_a(&mut self, a: u32) {
75//! self.a = a;
76//! self.updated_a();
77//! }
78//! }
79//! ```
80//!
81//! For **dynamic** properties, the value is set by calling the specified dynamic function, then the field updated function is called (listed above).
82//!
83//! ```ignore
84//! impl Demo {
85//! #[inline]
86//! pub fn update_c(&mut self) {
87//! self.calculate_c();
88//! self.updated_c();
89//! }
90//! }
91//! ```
92//!
93//! Note: be careful not to create cyclic dependencies!
94//!
95//! # Configuration
96//!
97//! The names of the generated functions can be customised by declaring a struct attribute and overriding a prefix/suffix. e.g:
98//!
99//! ```ignore
100//! #[derive(Dynamic)]
101//! #[dynamic(setter_prefix = "set_", setter_suffix = "_value")]
102//! struct MyStruct {
103//! a: u32,
104//! b: u32,
105//! }
106//!
107//! fn main() {
108//! let test = MyStruct { a: 1, b: 2 };
109//!
110//! test.set_a_value(3);
111//! test.set_b_value(4);
112//! }
113//! ```
114//!
115//! Properties that can specified include:
116//!
117//! | Name | Type | Comment |
118//! | - | - | - |
119//! | updated_prefix | str | Prefix for updated methods |
120//! | updated_suffix | str | Suffix for updated methods |
121//! | setter_prefix | str | Prefix for setter methods (non-dynamic fields) |
122//! | setter_suffix | str | Suffix for setter methods (non-dynamic fields) |
123//! | update_prefix | str | Prefix for update methods (dynamic fields) |
124//! | update_suffix | str | Suffix for update methods (dynamic fields) |
125//!
126use bae::FromAttributes;
127use quote::{format_ident, quote};
128use std::collections::{HashMap, HashSet};
129use syn::{
130 parenthesized,
131 parse::{Parse, ParseStream},
132 parse_macro_input,
133 punctuated::Punctuated,
134 token, Data, DeriveInput, Fields, Ident, LitStr, Token,
135};
136
137#[derive(FromAttributes, Default, Debug)]
138struct Dynamic {
139 updated_prefix: Option<LitStr>,
140 updated_suffix: Option<LitStr>,
141 setter_prefix: Option<LitStr>,
142 setter_suffix: Option<LitStr>,
143 update_prefix: Option<LitStr>,
144 update_suffix: Option<LitStr>,
145}
146
147struct DynamicField {
148 _paren_token: token::Paren,
149 dependencies: Punctuated<Ident, Token![,]>,
150 _comma: Token![,],
151 method_name: Ident,
152}
153
154impl Parse for DynamicField {
155 fn parse(input: ParseStream) -> syn::Result<Self> {
156 let content;
157 Ok(DynamicField {
158 _paren_token: parenthesized!(content in input),
159 dependencies: content.parse_terminated(Ident::parse)?,
160 _comma: input.parse()?,
161 method_name: input.parse()?,
162 })
163 }
164}
165
166const DYNAMIC_ATTR_NAME: &str = "dynamic";
167
168const DEFAULT_UPDATED_METHOD_PREFIX: &str = "updated_";
169const DEFAULT_UPDATED_METHOD_SUFFIX: &str = "";
170
171const DEFAULT_UPDATE_METHOD_PREFIX: &str = "update_";
172const DEFAULT_UPDATE_METHOD_SUFFIX: &str = "";
173
174const DEFAULT_SETTER_METHOD_PREFIX: &str = "update_";
175const DEFAULT_SETTER_METHOD_SUFFIX: &str = "";
176
177fn create_ident(ident: &Ident, prefix: &str, suffix: &str) -> Ident {
178 format_ident!("{}{}{}", prefix, ident, suffix)
179}
180
181#[proc_macro_derive(Dynamic, attributes(dynamic))]
182pub fn derive_dynamic(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
183 let DeriveInput {
184 ident, data, attrs, ..
185 } = parse_macro_input!(input);
186
187 //parse and merge the dynamic attribute for the struct
188 let config = Dynamic::try_from_attributes(&attrs)
189 .unwrap()
190 .unwrap_or_default();
191
192 let updated_method_prefix = config
193 .updated_prefix
194 .as_ref()
195 .map(|prefix| prefix.value())
196 .unwrap_or_else(|| DEFAULT_UPDATED_METHOD_PREFIX.to_string());
197 let updated_method_suffix = config
198 .updated_suffix
199 .as_ref()
200 .map(|prefix| prefix.value())
201 .unwrap_or_else(|| DEFAULT_UPDATED_METHOD_SUFFIX.to_string());
202 let setter_method_prefix = config
203 .setter_prefix
204 .as_ref()
205 .map(|prefix| prefix.value())
206 .unwrap_or_else(|| DEFAULT_SETTER_METHOD_PREFIX.to_string());
207 let setter_method_suffix = config
208 .setter_suffix
209 .as_ref()
210 .map(|prefix| prefix.value())
211 .unwrap_or_else(|| DEFAULT_SETTER_METHOD_SUFFIX.to_string());
212 let update_method_prefix = config
213 .update_prefix
214 .as_ref()
215 .map(|prefix| prefix.value())
216 .unwrap_or_else(|| DEFAULT_UPDATE_METHOD_PREFIX.to_string());
217 let update_method_suffix = config
218 .update_suffix
219 .as_ref()
220 .map(|prefix| prefix.value())
221 .unwrap_or_else(|| DEFAULT_UPDATE_METHOD_SUFFIX.to_string());
222
223 let create_updated_ident =
224 |ident: &Ident| create_ident(ident, &updated_method_prefix, &updated_method_suffix);
225
226 let create_setter_ident = |ident: &Ident| -> Ident {
227 create_ident(ident, &setter_method_prefix, &setter_method_suffix)
228 };
229
230 let create_update_ident = |ident: &Ident| -> Ident {
231 create_ident(ident, &update_method_prefix, &update_method_suffix)
232 };
233
234 //validate the usage of this macro and extract the field attributes
235 let fields = match data {
236 Data::Struct(data_struct) => match data_struct.fields {
237 Fields::Named(fields) => fields.named,
238 _ => panic!("Only structs with named fields currently supported!"),
239 },
240 _ => panic!("Only structs currently supported!"),
241 };
242
243 //parse the field 'dynamic' attributes
244 let (dynamic_fields, non_dynamic_fields): (Vec<_>, Vec<_>) = fields
245 .iter()
246 .map(|field| {
247 //merge the attributes that are marked as dynamic for the field
248 let dynamic = field
249 .attrs
250 .iter()
251 .find(|attr| {
252 attr.path
253 .get_ident()
254 .filter(|item| *item == DYNAMIC_ATTR_NAME)
255 .is_some()
256 })
257 .map(|attr| {
258 attr.parse_args::<DynamicField>()
259 .expect("Dynamic attribute format is invalid")
260 });
261
262 (field, dynamic)
263 })
264 .partition(|(_, dynamic)| dynamic.is_some());
265
266 //create a list of vars to update based on the dependencies
267 let mut inv_map: HashMap<&Ident, HashSet<&Ident>> = HashMap::new();
268
269 dynamic_fields.iter().for_each(|(field, dynamic)| {
270 let field_name = field.ident.as_ref().unwrap();
271
272 dynamic
273 .as_ref()
274 .unwrap()
275 .dependencies
276 .iter()
277 .for_each(|dependency| {
278 inv_map
279 .entry(dependency)
280 .and_modify(|impacts| {
281 impacts.insert(field_name);
282 })
283 .or_insert_with(|| HashSet::from([field_name]));
284 });
285 });
286
287 //updated methods based on the dependencies
288 let updated_methods = fields.iter().map(|field| {
289 let field_name = field.ident.as_ref().unwrap();
290 let func_name = create_updated_ident(field_name);
291 let deps = inv_map
292 .remove(field_name)
293 .unwrap_or_default()
294 .into_iter()
295 .map(create_update_ident);
296
297 quote! {
298 #[inline]
299 pub fn #func_name(&mut self) {
300 #(
301 self.#deps();
302 )*
303 }
304 }
305 });
306
307 //setters functions for non-dynamic functions that trigger the change functions
308 let setter_methods = non_dynamic_fields.iter().map(|(field, _)| {
309 let field_name = field.ident.as_ref().unwrap();
310 let func_name = create_setter_ident(field_name);
311 let updated_func_name = create_updated_ident(field_name);
312 let typ = &field.ty;
313
314 quote! {
315 #[inline]
316 pub fn #func_name(&mut self, value: #typ) {
317 self.#field_name = value;
318 self.#updated_func_name();
319 }
320 }
321 });
322
323 //update methods for dynamics (calls our desired function)
324 let update_methods = dynamic_fields.iter().map(|(field, dynamic)| {
325 let field_name = field.ident.as_ref().unwrap();
326 let func_name = create_update_ident(field_name);
327 let updated_func_name = create_updated_ident(field_name);
328 let callable_name = &dynamic.as_ref().unwrap().method_name;
329
330 quote! {
331 #[inline]
332 pub fn #func_name(&mut self) {
333 self.#callable_name();
334 self.#updated_func_name();
335 }
336 }
337 });
338
339 let output = quote! {
340 impl #ident {
341 #(
342 #updated_methods
343 )*
344 #(
345 #setter_methods
346 )*
347 #(
348 #update_methods
349 )*
350 }
351 };
352
353 output.into()
354}