macro dynamic-struct

A derive macro for creating push-based reactive properties for structs with named fields

1 unstable release

0.1.0 Jan 7, 2023

#1881 in Rust patterns

MIT/Apache

17KB
193 lines

Dynamic struct

A derive macro for creating push-based reactive properties for structs (with named fields only).

Why push-based?

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.

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)

The types can also be left untouched, no need for wrapping and dereferencing.

How to use

1) Add as a dependency to the Cargo file

[dependencies]
dynamic-struct = "0.1"

2) Add the derive macro to the struct and mark the properties that are dynamic

use dynamic_struct::Dynamic;

#[derive(Dynamic)]
struct Demo {
    a: u32,
    b: u32,
    #[dynamic((a, b), calculate_c)]
    c: u32,
}

impl Demo {
    fn calculate_c(&mut self) {
        self.c = self.a + self.b
    }
}

The attribute for the properties has the following structure:

#[dynamic(tuple of dependent property names, name of local method name)]

The local method must have the call signature matching fn name(&mut self).

3) Update the properties using the generated mutate functions

fn main() {
    let demo = Demo { a: 1, b: 2, c: 3 };

    dbg!(demo.c); //3
    demo.update_a(7);
    dbg!(demo.c); //9
}

How it works

1) Functions are created to signal when a property is changed, it is populated with the methods that should be called.

impl Demo {
    #[inline]
    pub fn updated_a(&mut self) {
        self.update_c();
    }
}

Note: properties that do not propagate changes will still be created but will be empty.

2) Functions are created for each property to update the property

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).

impl Demo {
    #[inline]
    pub fn update_a(&mut self, a: u32) {
        self.a = a;
        self.updated_a();
    }
}

For dynamic properties, the value is set by calling the specified dynamic function, then the field updated function is called (listed above).

impl Demo {
    #[inline]
    pub fn update_c(&mut self) {
        self.calculate_c();
        self.updated_c();
    }
}

Note: be careful not to create cyclic dependencies!

Configuration

The names of the generated functions can be customised by declaring a struct attribute and overriding a prefix/suffix. e.g:

#[derive(Dynamic)]
#[dynamic(setter_prefix = "set_", setter_suffix = "_value")]
struct MyStruct {
    a: u32,
    b: u32,
}

fn main() {
    let test = MyStruct { a: 1, b: 2 };

    test.set_a_value(3);
    test.set_b_value(4);
}

Properties that can specified include:

Name Type Comment
updated_prefix str Prefix for updated methods
updated_suffix str Suffix for updated methods
setter_prefix str Prefix for setter methods (non-dynamic fields)
setter_suffix str Suffix for setter methods (non-dynamic fields)
update_prefix str Prefix for update methods (dynamic fields)
update_suffix str Suffix for update methods (dynamic fields)

Dependencies

~2MB
~42K SLoC