Skip to main content

oxide_mvu/
lib.rs

1#![cfg_attr(feature = "no_std", no_std)]
2
3//! A lightweight Model-View-Update (MVU) runtime for Rust with `no_std` support.
4//!
5//! Implements the MVU pattern for building predictable, testable applications with
6//! unidirectional data flow and controlled side effects.
7//!
8//! # Overview
9//!
10//! The Model-View-Update (MVU) pattern, also known as the Elm Architecture, structures
11//! applications as a pure functional loop with three main components:
12//!
13//! - **Model**: Immutable state representing your entire application
14//! - **Update**: Pure function that transforms `(Event, Model) → (Model, Effect)`
15//! - **View**: Pure function that derives renderable Props from the Model
16//!
17//! This architecture makes state transitions predictable, debuggable, and testable by
18//! eliminating implicit state mutation and enforcing unidirectional data flow.
19//!
20//! # Core Concepts
21//!
22//! ## Model
23//!
24//! The Model is an immutable data structure representing your application's complete state.
25//! All behavior is a pure function of this state - nothing happens outside of it.
26//!
27//! ```rust
28//! #[derive(Clone)]
29//! struct Model {
30//!     counter: i32,
31//!     user_name: String,
32//!     is_loading: bool,
33//! }
34//! ```
35//!
36//! ## Event
37//!
38//! Events are discrete, immutable messages that trigger state transitions. They represent inputs
39//! from users, systems, or asynchronous effects. Events must implement the `Clone` trait.
40//!
41//! ```rust
42//! #[derive(Clone)]
43//! enum Event {
44//!     UserClickedButton,
45//!     DataLoaded(String),
46//!     TimerTicked,
47//! }
48//! ```
49//!
50//! ## Effect
51//!
52//! Effects declaratively describe side-effecting work (async I/O, timers, etc.) that produces
53//! events. They maintain purity in your update logic while enabling real-world interactions.
54//!
55//! Depending on your spawner implementation, effects may be processed in parallel.
56//!
57//! ```rust
58//! # use oxide_mvu::Effect;
59//! # #[derive(Clone)] enum Event { DataLoaded(String) }
60//! // Describe an async operation that will emit an event
61//! let effect = Effect::from_async(async move |emitter| {
62//!     let data = fetch_data().await;
63//!     emitter.emit(Event::DataLoaded(data)).await;
64//! });
65//! # async fn fetch_data() -> String { String::new() }
66//! ```
67//!
68//! ## Props
69//!
70//! Props are a pure, derived projection of the Model optimized for rendering or external
71//! presentation. They describe WHAT to render without prescribing HOW. Props commonly
72//! contain data and callbacks created via [`Emitter`].
73//!
74//! Props are not limited to UI - they can represent any external projection (API responses,
75//! hardware states, serialized output).
76//!
77//! # Architecture
78//!
79//! The MVU runtime orchestrates a unidirectional event loop:
80//!
81//! ```text
82//! ┌──────────────────────────────────────────────────────┐
83//! │                                                      │
84//! │  User Input / External Signal   ◀──────────┐         │
85//! │ (ie. Props callback or effect)             │         │
86//! │              │                             │         │
87//! │              ▼                             │         │
88//! │         ┌────────┐                         │         │
89//! │         │ Event  │                         │         │
90//! │         └────┬───┘                         │         │
91//! │              │  // sequenced by            │         │
92//! │              ▼  // the runtime             │         │
93//! │      ┌───────────────┐                     │         │
94//! │      │ update(event, │             ┌───────┴─────┐   │
95//! │      │     model)    │             │ emit(event) │   │
96//! │      └───────┬───────┘             └─────────────┘   │
97//! │              │                             ▲         │
98//! │              ▼                             │         │
99//! │    ┌─────────────────────┐                 │         │
100//! │    │ (Model, Effect)     │                 │         │
101//! │    └──────┬──────┬───────┘                 │         │
102//! │           │      │                         │         │
103//! │           │      └──► Effect task spawn ───┘         │
104//! │           │                                ▲         │
105//! │           ▼                                │         │
106//! │      ┌─────────┐                           │         │
107//! │      │  Model  │                           │         │
108//! │      └────┬────┘                           │         │
109//! │           │                                │         │
110//! │           ▼                                │         │
111//! │    ┌──────────────┐                        │         │
112//! │    │ view(model,  │               ┌────────┴───────┐ │
113//! │    │   emitter)   │               │ props callback │ │
114//! │    └──────┬───────┘               │ uses emitter   │ │
115//! │           │                       └────────────────┘ │
116//! │           ▼                                ▲         │
117//! │       ┌───────┐                            │         │
118//! │       │ Props │  // emitter captured       │         │
119//! │       └───┬───┘  // in Props callbacks     │         │
120//! │           │                                │         │
121//! │           ▼                                │         │
122//! │   ┌───────────────┐                  ┌────────────┐  │
123//! │   │ render(props) │  ─────────────►  │ user input │  │
124//! │   └───────────────┘                  └────────────┘  │
125//! │                                                      │
126//! └──────────────────────────────────────────────────────┘
127//! ```
128//!
129//! # When to Use
130//!
131//! **MVU is ideal for:**
132//! - Applications requiring predictable, debuggable state management
133//! - Event-driven systems (GUIs, embedded controllers, game loops)
134//! - Applications where state can be serialized/replayed (time-travel debugging)
135//! - Teams prioritizing testability and clear separation of concerns
136//! - `no_std` environments that can afford minimal heap allocations and meet prior criteria
137//!
138//! **Consider alternatives if:**
139//! - You need direct object mutation for performance-critical inner loops
140//! - Your application is primarily synchronous with minimal state
141//!
142//! # Platform Support
143//!
144//! ## Standard Environments
145//!
146//! By default, `oxide-mvu` works with the standard library.
147//!
148//! ```toml
149//! [dependencies]
150//! oxide-mvu = "0.4.2"
151//! ```
152//!
153//! ## `no_std` Environments
154//!
155//! For embedded systems or environments without the standard library, enable the
156//! `no_std` feature. This requires an allocator (`alloc` crate) but replaces all
157//! standard library dependencies with `no_std` crates:
158//!
159//! ```toml
160//! [dependencies]
161//! oxide-mvu = { version = "0.4.2", features = ["no_std"] }
162//! ```
163//!
164//! The runtime uses lock-free concurrency and bounded channels to minimize the cost of event
165//! synchronization. This makes the framework concurrency-model agnostic. Effects may execute in
166//! parallel or concurrently on the same thread as the runtime depending on hardware availability
167//! and your spawner implementation.
168//!
169//! # Quick Start
170//!
171//! ```rust
172//! use oxide_mvu::{Emitter, Effect, MvuLogic, MvuRuntime, Renderer};
173//!
174//! #[derive(Clone)]
175//! enum Event {
176//!     AccumulateClicked,
177//! }
178//!
179//! #[derive(Clone)]
180//! struct Model {
181//!     count: i32,
182//! }
183//!
184//! struct Props {
185//!     count: i32,
186//!     on_accumulate_click: Box<dyn Fn()>,
187//! }
188//!
189//! struct MyLogic;
190//!
191//! impl MvuLogic<Event, Model, Props> for MyLogic {
192//!     fn init(&self, model: Model) -> (Model, Effect<Event>) {
193//!         (model, Effect::none())
194//!     }
195//!
196//!     fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
197//!         match event {
198//!             Event::AccumulateClicked => {
199//!                 let new_model = Model {
200//!                     count: model.count + 1,
201//!                     ..model.clone()
202//!                 };
203//!                 (new_model, Effect::none())
204//!             }
205//!         }
206//!     }
207//!
208//!     fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
209//!         let emitter = emitter.clone();
210//!         Props {
211//!             count: model.count,
212//!             on_accumulate_click: Box::new(move || {
213//!                 emitter.try_emit(Event::AccumulateClicked);
214//!             }),
215//!         }
216//!     }
217//! }
218//!
219//! struct MyRenderer;
220//!
221//! impl Renderer<Props> for MyRenderer {
222//!     fn render(&mut self, _props: Props) {}
223//! }
224//!
225//! async fn main_async() {
226//!     // Create a spawner for your async runtime.
227//!     // This is how `Effect`s are executed.
228//!     let spawner = |fut| {
229//!         // Spawn the future on your chosen runtime.
230//!         // Examples:
231//!         // tokio::spawn(fut);
232//!         // async_std::task::spawn(fut);
233//!         let _ = fut;
234//!     };
235//!
236//!     let runtime = MvuRuntime::builder(
237//!         Model { count: 0 },
238//!         MyLogic,
239//!         MyRenderer,
240//!         spawner,
241//!     ).build();
242//!
243//!     // `run()` returns a Future representing the event loop.
244//!     // It must be awaited inside an async context.
245//!     runtime.run().await;
246//! }
247//! ```
248//!
249//! In a real application, `main_async` would be executed by your async runtime
250//! (e.g. via `#[tokio::main]`, `async_std::main`, or an embedded executor).
251//!
252//! # Advanced Topics
253//!
254//! ## Effect Composition
255//!
256//! Multiple effects can be combined using [`Effect::batch`]:
257//!
258//! ```rust
259//! # use oxide_mvu::Effect;
260//! # #[derive(Clone)] enum Event { A, B, C }
261//! let effect = Effect::batch(vec![
262//!     Effect::just(Event::A),
263//!     Effect::just(Event::B),
264//!     Effect::just(Event::C),
265//! ]);
266//! ```
267//!
268//! ## Event Buffer Capacity
269//!
270//! The runtime uses a bounded channel to queue events. The default capacity
271//! ([`DEFAULT_EVENT_CAPACITY`] = 32) is sized for embedded systems with limited heap.
272//! Customize this via the builder:
273//!
274//! ```rust,no_run
275//! # use oxide_mvu::{MvuRuntime, MvuLogic, Renderer, Effect, Emitter};
276//! # #[derive(Clone)] struct Model;
277//! # #[derive(Clone)] enum Event {}
278//! # struct Props;
279//! # struct Logic;
280//! # impl MvuLogic<Event, Model, Props> for Logic {
281//! #     fn init(&self, m: Model) -> (Model, Effect<Event>) { (m, Effect::none()) }
282//! #     fn update(&self, _: Event, m: &Model) -> (Model, Effect<Event>) { (m.clone(), Effect::none()) }
283//! #     fn view(&self, _: &Model, _: &Emitter<Event>) -> Props { Props }
284//! # }
285//! # struct MyRenderer;
286//! # impl Renderer<Props> for MyRenderer { fn render(&mut self, _: Props) {} }
287//! # let spawner = |_| {};
288//! // Memory-constrained embedded systems
289//! let runtime = MvuRuntime::builder(Model, Logic, MyRenderer, spawner)
290//!     .capacity(8)
291//!     .build();
292//!
293//! // High-throughput applications with event bursts
294//! let runtime = MvuRuntime::builder(Model, Logic, MyRenderer, spawner)
295//!     .capacity(1024)
296//!     .build();
297//! ```
298//!
299//! When the buffer is full:
300//! - [`Emitter::try_emit`] returns `false` and drops the event
301//! - [`Emitter::emit`] awaits until space is available (backpressure)
302//!
303//! ## Testing
304//!
305//! Enable the `testing` feature to access test utilities:
306//!
307//! ```toml
308//! [dev-dependencies]
309//! oxide-mvu = { version = "0.4.2", features = ["testing"] }
310//! ```
311//!
312//! Test your MVU logic deterministically:
313//!
314//! ```rust
315//! # #[cfg(feature = "testing")]
316//! # {
317//! use oxide_mvu::{Effect, MvuLogic, Renderer, TestMvuRuntime, create_test_spawner};
318//! # #[derive(Clone)] enum Event { Increment }
319//! # #[derive(Clone)] struct Model { count: i32 }
320//! # struct Props;
321//! # struct Logic;
322//! # impl MvuLogic<Event, Model, Props> for Logic {
323//! #     fn init(&self, m: Model) -> (Model, Effect<Event>) { (m, Effect::none()) }
324//! #     fn update(&self, _: Event, m: &Model) -> (Model, Effect<Event>) {
325//! #         (Model { count: m.count + 1 }, Effect::none())
326//! #     }
327//! #     fn view(&self, _: &Model, _: &oxide_mvu::Emitter<Event>) -> Props { Props }
328//! # }
329//! # struct MyRenderer;
330//! # impl Renderer<Props> for MyRenderer { fn render(&mut self, _: Props) {} }
331//!
332//! let runtime = TestMvuRuntime::builder(
333//!     Model { count: 0 },
334//!     Logic,
335//!     MyRenderer,
336//!     create_test_spawner()
337//! ).build();
338//!
339//! let mut driver = runtime.run();
340//! // Manually process events in tests
341//! driver.process_events();
342//! # }
343//! ```
344//!
345//! See `TestMvuRuntime` (available with the `testing` feature) for comprehensive testing utilities.
346//!
347//! ## Async Runtime Integration
348//!
349//! The [`Spawner`] trait abstracts over different async runtimes. Common patterns:
350//!
351//! ```rust,ignore
352//! // tokio
353//! let spawner = |fut| { tokio::spawn(fut); };
354//!
355//! // async-std
356//! let spawner = |fut| { async_std::task::spawn(fut); };
357//!
358//! // smol
359//! let spawner = |fut| { smol::spawn(fut).detach(); };
360//! ```
361//!
362//! # See Also
363//!
364//! - [`MvuLogic`] - The core trait defining application behavior
365//! - [`Effect`] - Declarative side effect system
366//! - [`Emitter`] - Event dispatch from Props callbacks
367//! - [`Renderer`] - Integration point for rendering systems
368//! - [`MvuRuntime`] - The runtime orchestrating the event loop
369
370#[cfg(feature = "no_std")]
371extern crate alloc;
372
373/// Trait alias for event type constraints.
374///
375/// All events must implement these bounds to work with the MVU runtime.
376pub trait Event: Send + Sync + Clone + 'static {}
377
378/// Blanket implementation for any type that satisfies the bounds.
379impl<T> Event for T where T: Send + Sync + Clone + 'static {}
380
381// Module declarations
382mod effect;
383mod emitter;
384mod logic;
385mod renderer;
386mod runtime;
387
388// Public re-exports
389pub use effect::Effect;
390pub use emitter::Emitter;
391pub use logic::MvuLogic;
392pub use renderer::Renderer;
393pub use runtime::{MvuRuntime, MvuRuntimeBuilder, Spawner, DEFAULT_EVENT_CAPACITY};
394
395// Test utilities (only available with 'testing' feature or during tests)
396#[cfg(any(test, feature = "testing"))]
397pub use renderer::TestRenderer;
398#[cfg(any(test, feature = "testing"))]
399pub use runtime::{create_test_spawner, TestMvuDriver, TestMvuRuntime, TestMvuRuntimeBuilder};