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}