# oxide-mvu
A lightweight Model-View-Update (MVU) runtime framework for Rust with `no_std` support.
## Overview
`oxide-mvu` implements the MVU pattern for building predictable, testable applications in Rust. The framework is powerful enough
to be a good choice for most applications, but will always evolve with support for no_std, low-memory, single-cpu, embedded systems as a baseline.
Oxide is intended to clarify the management of state in an application using clear separation of concerns between the view and application logic layers.
## Features
- **Unidirectional data flow**: State transitions occur in a single loop:
- → event emission is triggered from an effect or user input
- → update function receives event data and yields new state
- → view function reduces state to renderable props
- → renderer function receives props for display
- **Type-safe event dispatch**: Callbacks in Props maintain type safety
- **Effect system**: Controlled, declarative side effects
- **no_std support**: Works in embedded environments (requires `alloc` for heap allocation)
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
oxide-mvu = "0.1.0"
```
For `no_std` environments:
```toml
[dependencies]
oxide-mvu = { version = "0.1.0", features = ["no_std"] }
```
## Usage
### Define your types
```rust
use oxide_mvu::{Emitter, Effect, MvuLogic};
#[derive(Clone)]
enum Event {
Increment,
Decrement,
}
#[derive(Clone)]
struct Model {
count: i32,
}
struct Props {
count: i32,
on_increment: Box<dyn Fn()>,
on_decrement: Box<dyn Fn()>,
}
```
### Implement the MvuLogic trait
```rust
struct Logic;
impl MvuLogic<Event, Model, Props> for Logic {
fn init(&self, model: Model) -> (Model, Effect<Event>) {
(model, Effect::none())
}
fn update(&self, event: Event, model: &Model) -> (Model, Effect<Event>) {
let new_model = match event {
Event::Increment => Model { count: model.count + 1 },
Event::Decrement => Model { count: model.count - 1 },
};
(new_model, Effect::none())
}
fn view(&self, model: &Model, emitter: &Emitter<Event>) -> Props {
let emitter_inc = emitter.clone();
let emitter_dec = emitter.clone();
Props {
count: model.count,
on_increment: Box::new(move || emitter_inc.emit(Event::Increment)),
on_decrement: Box::new(move || emitter_dec.emit(Event::Decrement)),
}
}
}
```
### Implement a Renderer
```rust
use oxide_mvu::Renderer;
struct MyRenderer;
impl Renderer<Props> for MyRenderer {
fn render(&mut self, props: Props) {
println!("Count: {}", props.count);
// In a real app, you'd render UI here and wire up callbacks:
// button.on_click = props.on_increment;
}
}
```
### Run the application
```rust
use oxide_mvu::MvuRuntime;
fn main() {
let model = Model { count: 0 };
let logic = Box::new(Logic);
let renderer = Box::new(MyRenderer);
let runtime = MvuRuntime::new(model, logic, renderer);
runtime.run();
}
```
## Testing
`oxide-mvu` provides specialized testing utilities for integration testing your MVU applications.
### Enabling Testing Utilities
To access the testing helpers in your project, enable the `testing` feature:
```toml
[dev-dependencies]
oxide-mvu = { version = "0.1.0", features = ["testing"] }
```
This gives you access to:
- `TestMvuRuntime` - Runtime with manual event processing control
- `TestMvuDriver` - Driver for manually processing events in tests
- `TestRenderer` - Renderer that captures Props for assertions
### Unit Testing
State transitions are pure functions, making them easy to unit test:
```rust
#[test]
fn test_increment() {
let logic = Logic;
let model = Model { count: 0 };
let (new_model, _effect) = logic.update(Event::Increment, &model);
assert_eq!(new_model.count, 1);
}
```
### Integration Testing
Use `TestMvuRuntime` and `TestRenderer` to test the full MVU loop:
```rust
use oxide_mvu::{TestMvuRuntime, TestRenderer, TestMvuDriver};
#[test]
fn test_full_mvu_loop() {
// Create a TestRenderer to capture renders
let renderer = TestRenderer::new();
// Create runtime with test helpers
let runtime = TestMvuRuntime::new(
Model { count: 0 },
Box::new(Logic),
renderer.boxed(),
);
// Run and get driver for manual event control
let mut driver = runtime.run();
// Verify initial render
assert_eq!(renderer.count(), 1);
renderer.with_renders(|renders| {
assert_eq!(renders[0].count, 0);
});
// Trigger event via Props callback
renderer.with_renders(|renders| {
(renders[0].on_increment)();
});
// Manually process events
driver.process_events();
// Verify updated render
assert_eq!(renderer.count(), 2);
renderer.with_renders(|renders| {
assert_eq!(renders[1].count, 1);
});
}
```
### Key Testing Concepts
- **`TestMvuRuntime`**: Unlike `MvuRuntime`, events are not automatically processed. You must call `driver.process_events()` to manually drive the event loop.
- **`TestRenderer::boxed()`**: Returns a boxed renderer while keeping a handle for assertions.
- **`renderer.with_renders()`**: Access all captured Props for assertions or to trigger callbacks.
- **`driver.process_events()`**: Process all queued events until the queue is empty.
See `tests/lib_test.rs` for complete examples.
## License
Licensed under the Apache License, Version 2.0 ([LICENSE](LICENSE) or http://www.apache.org/licenses/LICENSE-2.0)