1 unstable release
new 0.9.0 | Mar 12, 2025 |
---|
#960 in Rust patterns
19KB
140 lines
Crete
Crete is a procedural macro that simplifies state management in Rust, in a flexible way.
It generates code for atomic access to a struct's fields using an RwLock
and a static store, making it easier to work
with shared state in both synchronous and asynchronous contexts.
Because users can implement practically anything on the struct, Crete allows for a flexible store that can be tailored to a variety of needs. Perhaps you'd like to use to build a Redux-style store. Perhaps you enjoy having a billion custom setters. Perhaps you just enjoy mutating everything directly. Or maybe you even have your own homemade style of setters and getters that you love best?
No restrictions.
Features
-
Ergonomic, intuitive Design
Reduces boilerplate with a straightforward interface for managing state. -
Synchronous and Asynchronous Support
Works seamlessly in both synchronous and asynchronous environments. -
Versatile Clone Support
Adapts to your needs by supporting both cloneable and non-cloneable types. -
No shoehorning
Build the State management style you enjoy best on top... or not.
Details
-
Generates code for atomic, thread-safe access to a struct by defining a static store typically within a module that holds and manages the shared state.
-
Generates unit structs (e.g.
FooField
,BarField
) and implements aField
trait for each, enabling type-safe access and update of individual fields.
This macro produces:
-
A static store (a
LazyLock
holding anRwLock
protecting anArc
of the struct) whose identifier is based on the struct name (e.g.CRETE_FOO
for a struct namedFoo
). -
An implementation of several associated methods on the struct:
new()
: constructs a new instance using the struct'sDefault
implementation. Called byLazyLock
.read()
: returns anArc
-wrapped shared reference to the current state.clone()
: (if the struct implementsClone
) returns a cloned instance of the stored value.write(self)
: atomically replaces the current stored value with the provided one.select_ref<F: Field>(&self, field: F) -> &F::FieldType
: returns a reference to the selected field.get<F, R>(field: F, f: impl FnOnce(&F::FieldType) -> R) -> R
: applies a closure to a shared reference of the selected field.select<F: Field>(field: F) -> F::FieldType
: returns a cloned value of the selected field.set<F>(field: F, value: F::FieldType)
: updates a specific field and writes the new state to the store.update(f: impl FnOnce(&mut Self))
: applies a mutation closure to the current state and updates the store.update_async(f: impl AsyncFnOnce(&mut Self))
: an asynchronous version ofupdate
for non-blocking mutations.
The generated code leverages std::sync::LazyLock
, RwLock
, and Arc
to ensure that all operations
are safe to use concurrently from multiple threads.
How it works
Static Store
A static store is created for the struct, allowing atomic access to its state:
static #crete_store_ident: ::std::sync::LazyLock<::std::sync::RwLock<::std::sync::Arc<#struct_name>>> =
::std::sync::LazyLock::new(|| ::std::sync::RwLock::new(::std::sync::Arc::new(#struct_name::new())));
With a struct like this:
use crete::Crete;
#[derive(Crete, Default, Clone, Debug)]
pub struct Store {
pub field1: String,
pub field_foo: String,
pub toggle: bool,
pub index: u32
}
impl Store {
pub async fn inc1(&mut self) {
self.index += 1;
}
pub fn dec2(&mut self) {
self.index -= 2;
}
}
You can now do this:
#[test]
#[serial]
fn doc() {
// Some initial values
Store::set(Field1Field, "test value".to_string());
Store::set(FieldFooField, "Foo".to_string());
Store::set(ToggleField, true);
Store::set(IndexField, 1000);
/******************/
/****** READ ******/
/******************/
// Get an owned value (only available if a field is Clone)
let field1_value = Store::select(Field1Field);
let field_foo_value = Store::select(FieldFooField);
let toggle_value = Store::select(ToggleField);
let index_value = Store::select(IndexField);
assert_eq!(field1_value, "test value".to_string());
assert_eq!(field_foo_value, "Foo".to_string());
assert_eq!(toggle_value, true);
assert_eq!(index_value, 1000);
// Use the closure-based `get` method to get a reference
Store::get(Field1Field, |value| {
// `value` is a reference to the field
assert_eq!(value, &"test value".to_string());
});
// You could also get a reference via binding since this is an RWLock<Arc<F>
{
let binding = Store::read();
let field1_ref = binding.select_ref(Field1Field);
assert_eq!(field1_ref, &"test value".to_string());
}
/******************/
/****** WRITE *****/
/******************/
// Update via closure
Store::update(|s| { // &mut Store
s.field1 = "updated value".to_string();
s.dec2();
});
assert_eq!(Store::select(Field1Field), "updated value".to_string());
assert_eq!(Store::read().index, 998);
// And we can use `set()` as we saw earlier
Store::set(FieldFooField, "Foo".to_string());
assert_eq!(Store::select(FieldFooField), "Foo".to_string());
}
Async closure support
#[tokio::test]
async fn doc2() {
Store::set(IndexField, 1000);
// Async closure
Store::update_async(async |s| { // &mut Store
s.field1 = "updated value 2".to_string();
s.inc1().await;
s.inc1().await;
s.inc1().await;
}).await;
assert_eq!(Store::select(IndexField), 1003);
Store::get(Field1Field, |value| {
assert_eq!(value, "updated value 2");
});
}
No Clone, No Problem
It works the same way, except:
select()
does not exist for fields that are not clone-able.clone()
does not exist for the static Struct.
#[cfg(test)]
mod tests_no_clone {
use tokio;
use crete::Crete;
#[derive(Debug, PartialEq)]
pub struct NotCloneType {
pub value: i32,
}
impl Default for NotCloneType {
fn default() -> Self {
NotCloneType { value: 0 }
}
}
#[derive(Crete, Default)]
pub struct Store {
pub foo: NotCloneType,
}
impl Store {
async fn reset(&mut self) {
self.foo = NotCloneType::default();
}
fn init(&mut self) {
self.foo = NotCloneType { value: 42 };
}
}
#[tokio::test]
async fn foobar() {
Store::set(FooField, NotCloneType { value: 100 });
Store::get(FooField, |v| {
assert_eq!(v, &NotCloneType { value: 100 });
});
Store::update_async(async |s| {
s.init();
assert_eq!(s.foo, NotCloneType { value: 42 });
s.reset().await;
}).await;
Store::get(FooField, |v| {
assert_eq!(v, &NotCloneType { value: 0 });
});
}
}
Considerations
You are using RWLock
behind the scenes. The usual considerations for locking in multithreaded code apply.
In essence:
- Don't let your thread panic while it holds a write lock.
- If the thread already has acquired a lock, don't try to get it again before it drops.
FAQ
What's with the name?
- It's interestingly confusing with
crate
. - It's the name of the island the crate author spends lots of his time at.
License
This project is licensed under the MIT License._
Dependencies
~0.6–1MB
~21K SLoC