x_bow/
lib.rs

1//! # X-Bow: precise state management
2//!
3//! X-Bow is a state management library aimed for use in UI programming.
4//! It let you...
5//! * keep your data in a centralized store.
6//! * build "paths" that point to parts of the store.
7//! * borrow and mutate data at those paths.
8//! * subscribe to mutations at those paths through async API.
9//!
10//! ## Quick Example
11//!
12//! ```
13//! # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
14//! # #[derive(Default, Trackable)]
15//! # struct AnotherStruct {
16//! #     asdf: String
17//! # }
18//! // Derive `Trackable` to allow parts of the struct to be tracked.
19//! #[derive(Default, Trackable)]
20//! #[track(deep)] // `deep` option is useful if the fields themselves are structs
21//! struct MyStruct {
22//!     field_1: i32,
23//!     field_2: u64,
24//!     child_struct: AnotherStruct
25//! }
26//!
27//! // Create a centralized store with the data.
28//! let store = Store::new(MyStruct::default());
29//!
30//! // Build a path to the `i32` at `field_1` in the `MyStruct`.
31//! let path = store.build_path().field_1();
32//!
33//! // This implements the `Stream` trait. You can do `stream.next().await`, etc.
34//! let stream = path.until_change();
35//!
36//! // Mutably borrow the `i32` of the path, and increment it.
37//! // This will cause the `stream` we created to fire.
38//! *path.borrow_mut() += 1;
39//! ```
40//!
41//!
42//! ## Concepts
43//!
44//! ### Store
45//! The store is where the application state lives. It is a big RefCell.
46//!
47//! ### Paths
48//! A path identifies a piece of data in your store. It implements [PathExt],
49//! which contains most of the methods you will interact with.
50//!
51//! Paths are usually wrapped in `PathBuilder` objects. These objects each
52//! dereference to the path object it wraps.
53//!
54//! ### Path Builders
55//! A path builder wraps a path object and derefs to the path object.
56//!
57//! It also provides methods that to "continue" the path; if the path points to
58//! `T`, the PathBuilder will let you convert it to a path that points to some
59//! part inside `T`.
60//!
61//! For example: the path builder that wraps a path to `Vec<T>` has an
62//! `index(idx: usize)` method that returns a path to `T`.
63//!
64//! To convert from a PathBuilder to a Path, use [IntoPath::into_path].
65//! To convert from a Path to a PathBuilder, use [PathExt::build_path].
66//!
67//! ### Trackable Types
68//! Types that implements [Trackable] have their corresponding `PathBuilder` type.
69//! To be `Trackable`, types should have `#[derive(Trackable)]` on them.
70//!
71//! ## Usage
72//! ### Steps
73//! 1.  Make your structs and enums trackable by putting
74//!     `#[derive(Trackable)]` and `[track(deep)]` on them.
75//!     See documentation for the [Trackable macro][derive@Trackable].
76//!     ```
77//!     # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
78//!     // 👇 Derive `Trackable` to allow parts of the struct to be tracked.
79//!     #[derive(Trackable)]
80//!     #[track(deep)]
81//!     struct MyStruct {
82//!         field_1: i32,
83//!         field_2: u64,
84//!         child_enum: MyEnum
85//!     }
86//!     // 👇 Derive `Trackable` to allow parts of the enum to be tracked.
87//!     #[derive(Trackable)]
88//!     #[track(deep)]
89//!     enum MyEnum {
90//!         Variant1(i32),
91//!         Variant2 {
92//!             data: String
93//!         }
94//!     }
95//!     ```
96//! 2.  Put your data in a [Store].
97//!     ```
98//!     # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
99//!     # #[derive(Trackable)]
100//!     # #[track(deep)]
101//!     # struct MyStruct {
102//!     #     field_1: i32,
103//!     #     field_2: u64,
104//!     #     child_enum: MyEnum
105//!     # }
106//!     #
107//!     # #[derive(Trackable)]
108//!     # #[track(deep)]
109//!     # enum MyEnum {
110//!     #     Variant1(i32),
111//!     #     Variant2 {
112//!     #         data: String
113//!     #     }
114//!     # }
115//!     let my_data = MyStruct {
116//!         field_1: 42,
117//!         field_2: 123,
118//!         child_enum: MyEnum::Variant2 { data: "Hello".to_string() }
119//!     };
120//!     let store = Store::new(my_data);
121//!     ```
122//! 3.  Make [Path]s.
123//!     ```
124//!     # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
125//!     # #[derive(Trackable)]
126//!     # #[track(deep)]
127//!     # struct MyStruct {
128//!     #     field_1: i32,
129//!     #     field_2: u64,
130//!     #     child_enum: MyEnum
131//!     # }
132//!     #
133//!     # #[derive(Trackable)]
134//!     # #[track(deep)]
135//!     # enum MyEnum {
136//!     #     Variant1(i32),
137//!     #     Variant2 {
138//!     #         data: String
139//!     #     }
140//!     # }
141//!     let my_data = MyStruct {
142//!         field_1: 42,
143//!         field_2: 123,
144//!         child_enum: MyEnum::Variant2 { data: "Hello".to_string() }
145//!     };
146//!     # let store = Store::new(my_data);
147//!     let path_to_field_1 = store.build_path().field_1();
148//!     let path_to_data = store.build_path().child_enum().Variant2_data();
149//!     ```
150//! 4.  Use the Paths you made.
151//!     See [PathExt] and [PathExtGuaranteed] for available APIs.
152//!
153//! ### Tracking through Vec and HashMap
154//! You can track through [Vec] using the `index(_)` method.
155//! ```
156//! # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
157//! let store = Store::new(vec![1, 2, 3]);
158//! let path = store.build_path().index(1); // 👈 path to the second element in the vec
159//! ```
160//! You can track through [HashMap][std::collections::HashMap] using the `key(_)` method.
161//! ```
162//! # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
163//! # use std::collections::HashMap;
164//! let store = Store::new(HashMap::<u32, String>::new());
165//! let path = store.build_path().key(555); // 👈 path to the String at key 555 in the hashmap.
166//! ```
167//!
168//! ## Design
169//!
170//! ### Borrowing and Paths
171//! The design of this library is a hybrid between simple [RefCell][std::cell::RefCell]
172//! and the "lens" concept prevalent in the functional programming world.
173//!
174//! The centralized data store of X-Bow is a RefCell.
175//! The library provides RefCell-like APIs like
176//! [borrow][PathExtGuaranteed::borrow] and [borrow_mut][PathExtGuaranteed::borrow_mut].
177//! Mutation is well supported and immutable data structures are not needed.
178//!
179//! The library uses *Path*s to identify parts of the data in the store.
180//! Paths are made from composing segments. Each segment is like a "lens",
181//! knowing how to project into some substructure of a type.
182//! Composed together, these segments become a path that knows how to project
183//! from the root data in the store to the part that it identifies.
184//!
185//! The difference between Paths and Lens/Optics is just that our paths work
186//! mutably, while Lens/Optics are often associated with immutable/functional design.
187//!
188//! ### Notification and Subscription
189//! Another important aspect of the library design is the notification/subscription
190//! functionality provided through [until_change][PathExt::until_change]
191//! and [until_bubbling_change][PathExt::until_bubbling_change].
192//!
193//! Change listening is done based on Paths' hashes. We have a map associating
194//! each hash to a version number and a list of listening [Waker][std::task::Waker]s.
195//! The [until_change][PathExt::until_change] method registers wakers at the
196//! hash of its target path and all prefix paths (when some piece of data
197//! encompassing the target data is changed, we assume the target data is changed too).
198//!
199//! #### Hash Collision
200//! If two paths end up with the same hash, wake notification to one would wake
201//! listeners to the other too. Thus, the `until_change` stream may fire spuriously.
202//!
203//! Keep in mind that the probability of `u64` hash collision is extremely low;
204//! with 10,000 distinct paths in a store, collision probability can
205//! [be calculated](https://en.wikipedia.org/wiki/Birthday_problem#Probability_of_a_shared_birthday_(collision))
206//! to be less than 1E-11 (0.000000001%).
207//!
208//! To further minimize the impact of hash collisions, X-Bow saves the length
209//! of paths along with their hashes. This increases collision probability, but
210//! it ensures that paths of different lengths never collide; modifying some
211//! data deep in the state tree would never result in the entire tree being
212//! woken.
213
214mod borrow_mut_guard;
215mod guarantee;
216mod hash;
217mod hash_visitor;
218mod impls;
219mod path;
220mod path_ext;
221mod path_impl;
222mod store;
223mod trackable;
224mod until_change;
225mod wakers;
226
227pub use guarantee::PathExtGuaranteed;
228pub use path::Path;
229pub use path_ext::PathExt;
230pub use path_impl::ReferencePath;
231pub use store::{Store, StoreRoot};
232pub use trackable::{IntoPath, Trackable};
233
234pub mod path_ext_wrappers {
235    pub use super::path_ext::{
236        bind_for_each::BindForEach, for_each::ForEach, signal_stream::SignalStream,
237    };
238}
239
240/// Macro to allows building paths to fields inside a struct/enum.
241///
242/// ```
243/// # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
244/// // Derive `Trackable` to allow parts of the struct to be tracked.
245/// #[derive(Default, Trackable)]
246/// struct MyStruct {
247///     field_1: i32,
248///     field_2: u64
249/// }
250///
251/// // Create a centralized store with the data.
252/// let store = Store::new(MyStruct::default());
253///
254/// // Build a path to the `i32` at `field_1` in the `MyStruct`.
255/// let path = store.build_path().field_1();
256/// ```
257///
258/// ### Shallow and Deep Tracking
259///
260/// By default, the `Trackable` derive macro makes tracking "shallow".
261/// This means you can build path into each child field of a struct/enum,
262/// but if the type in a child field is itself a struct/enum, you won't be able
263/// to continue your path into a grandchild field.
264/// ```compile_fail
265/// # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
266/// #[derive(Default, Trackable)]
267/// struct MyStruct {
268///     field_1: ChildStruct,
269///     field_2: u64
270/// }
271/// #[derive(Default)]
272/// struct ChildStruct {
273///     asdf: String
274/// }
275///
276/// let store = Store::new(MyStruct::default());
277///
278/// store.build_path().field_1(); // OK
279/// store.build_path().field_1().asdf(); // cannot do this!!
280/// ```
281///
282/// To allow building paths deep into descendants, enable deep tracking
283/// ```
284/// # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
285/// #[derive(Default, Trackable)]
286/// struct MyStruct {
287///     #[track(deep)] // 👈 enable deep tracking
288///     field_1: ChildStruct,
289///     field_2: u64
290/// }
291/// #[derive(Default, Trackable /* 👈 the subject of deep tracking must be Trackable */)]
292/// struct ChildStruct {
293///     asdf: String
294/// }
295///
296/// let store = Store::new(MyStruct::default());
297///
298/// store.build_path().field_1(); // OK
299/// store.build_path().field_1().asdf(); // OK
300/// ```
301///
302/// Often, you will end up with `#[track(deep)]` on most (if not all) your
303/// fields. In this case, you can apply the attribute on the struct/enum itself.
304/// The attribute `#[track(shallow)]` can then be applied on individual fields
305/// to opt out of deep tracking.
306/// ```
307/// # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
308/// #[derive(Trackable)]
309/// #[track(deep)] // 👈 enable deep tracking
310/// struct MyStruct {
311///     field_1: ChildStruct,
312///     #[track(shallow)] // 👈 don't track this field deeply
313///     field_2: u64
314/// }
315/// # #[derive(Trackable)]
316/// # struct ChildStruct {
317/// #     asdf: String
318/// # }
319/// ```
320///
321/// ### Tracking Enums
322///
323/// As implied along this doc, the `Trackable` derive macro works on enums too.
324/// ```
325/// # use x_bow::{Trackable, Store, PathExt, PathExtGuaranteed};
326/// #[derive(Trackable)]
327/// enum MyEnum {
328///     Variant1 (String),
329///     Variant2 {
330///         field_a: i32,
331///         field_b: u64
332///     }
333/// }
334/// let store = Store::new(MyEnum::Variant1 ("Hello".into()));
335///
336/// let path = store.build_path().Variant1_0(); // <- the `String` in Variant1
337/// let path = store.build_path().Variant2_field_a(); // <- the `i32` in Variant2
338/// ```
339pub use x_bow_macros::Trackable;
340
341// TODO: figure out how to make Rust-Analyzer stop suggesting items from this module
342#[doc(hidden)]
343pub mod __private_macro_only {
344    #[doc(hidden)]
345    pub use super::guarantee::PathExtGuaranteed;
346    #[doc(hidden)]
347    pub use super::hash_visitor::HashVisitor;
348    #[doc(hidden)]
349    pub use super::impls::leaf::TrackableLeaf;
350    #[doc(hidden)]
351    pub use super::path::Path;
352    #[doc(hidden)]
353    pub use super::trackable::IntoPath;
354    #[doc(hidden)]
355    pub use super::trackable::Trackable;
356    #[doc(hidden)]
357    pub use super::wakers::StoreWakers;
358    #[doc(hidden)]
359    pub use x_bow_macros::IntoPath;
360}