use crate::Action;
use std::marker::PhantomData;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NoEffect {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReducerResult<E = NoEffect> {
pub changed: bool,
pub effects: Vec<E>,
}
impl<E> Default for ReducerResult<E> {
fn default() -> Self {
Self::unchanged()
}
}
impl<E> ReducerResult<E> {
#[inline]
pub fn unchanged() -> Self {
Self {
changed: false,
effects: vec![],
}
}
#[inline]
pub fn changed() -> Self {
Self {
changed: true,
effects: vec![],
}
}
#[inline]
pub fn effect(effect: E) -> Self {
Self {
changed: false,
effects: vec![effect],
}
}
#[inline]
pub fn effects(effects: Vec<E>) -> Self {
Self {
changed: false,
effects,
}
}
#[inline]
pub fn changed_with(effect: E) -> Self {
Self {
changed: true,
effects: vec![effect],
}
}
#[inline]
pub fn changed_with_many(effects: Vec<E>) -> Self {
Self {
changed: true,
effects,
}
}
#[inline]
pub fn with(mut self, effect: E) -> Self {
self.effects.push(effect);
self
}
#[inline]
pub fn mark_changed(mut self) -> Self {
self.changed = true;
self
}
#[inline]
pub fn has_effects(&self) -> bool {
!self.effects.is_empty()
}
}
pub type Reducer<S, A, E = NoEffect> = fn(&mut S, A) -> ReducerResult<E>;
pub(crate) const DEFAULT_MAX_DISPATCH_DEPTH: usize = 16;
pub(crate) const DEFAULT_MAX_DISPATCH_ACTIONS: usize = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DispatchLimits {
pub max_depth: usize,
pub max_actions: usize,
}
impl Default for DispatchLimits {
fn default() -> Self {
Self {
max_depth: DEFAULT_MAX_DISPATCH_DEPTH,
max_actions: DEFAULT_MAX_DISPATCH_ACTIONS,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DispatchError {
DepthExceeded {
max_depth: usize,
action: &'static str,
},
ActionBudgetExceeded {
max_actions: usize,
processed: usize,
action: &'static str,
},
}
impl std::fmt::Display for DispatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DispatchError::DepthExceeded { max_depth, action } => write!(
f,
"middleware dispatch depth limit exceeded (max_depth={max_depth}, action={action})"
),
DispatchError::ActionBudgetExceeded {
max_actions,
processed,
action,
} => write!(
f,
"middleware dispatch action budget exceeded (max_actions={max_actions}, processed={processed}, action={action})"
),
}
}
}
impl std::error::Error for DispatchError {}
pub(crate) fn check_dispatch_limits(
limits: DispatchLimits,
dispatch_depth: usize,
processed: usize,
action: &'static str,
) -> Result<(), DispatchError> {
if dispatch_depth >= limits.max_depth {
return Err(DispatchError::DepthExceeded {
max_depth: limits.max_depth,
action,
});
}
if processed >= limits.max_actions {
return Err(DispatchError::ActionBudgetExceeded {
max_actions: limits.max_actions,
processed,
action,
});
}
Ok(())
}
#[inline]
pub(crate) fn debug_assert_valid_dispatch_limits(limits: DispatchLimits) {
debug_assert!(
limits.max_depth >= 1 && limits.max_actions >= 1,
"DispatchLimits requires max_depth >= 1 and max_actions >= 1"
);
}
pub(crate) trait MiddlewareDispatchDriver<A: Action> {
type Output;
fn before(&mut self, action: &A) -> bool;
fn reduce(&mut self, action: A) -> Self::Output;
fn cancelled_output(&mut self) -> Self::Output;
fn after(&mut self, action: &A, result: &Self::Output) -> Vec<A>;
fn merge_child(&mut self, parent: &mut Self::Output, child: Self::Output);
}
enum DispatchFrame<A: Action, O> {
Pending(A),
Entered {
result: O,
injected: std::vec::IntoIter<A>,
},
}
pub(crate) fn run_iterative_middleware_dispatch<A, D>(
limits: DispatchLimits,
action: A,
driver: &mut D,
) -> Result<D::Output, DispatchError>
where
A: Action,
D: MiddlewareDispatchDriver<A>,
{
let mut processed = 0usize;
let mut stack = vec![DispatchFrame::<A, D::Output>::Pending(action)];
while let Some(frame) = stack.pop() {
match frame {
DispatchFrame::Pending(action) => {
let depth = stack.len();
check_dispatch_limits(limits, depth, processed, action.name())?;
processed += 1;
if !driver.before(&action) {
if !stack.is_empty() {
continue;
}
return Ok(driver.cancelled_output());
}
let result = driver.reduce(action.clone());
let injected = driver.after(&action, &result).into_iter();
stack.push(DispatchFrame::Entered { result, injected });
}
DispatchFrame::Entered {
result,
mut injected,
} => {
if let Some(injected_action) = injected.next() {
stack.push(DispatchFrame::Entered { result, injected });
stack.push(DispatchFrame::Pending(injected_action));
continue;
}
if let Some(DispatchFrame::Entered {
result: parent_result,
..
}) = stack.last_mut()
{
driver.merge_child(parent_result, result);
continue;
}
return Ok(result);
}
}
}
unreachable!("dispatch stack should not drain before a root result is returned")
}
#[macro_export]
macro_rules! reducer_compose {
($state:expr, $action:expr, { $($arms:tt)+ }) => {{
let __state = $state;
let __action_input = $action;
let __context = ();
$crate::reducer_compose!(@accum __state, __action_input, __context; () $($arms)+)
}};
($state:expr, $action:expr, $context:expr, { $($arms:tt)+ }) => {{
let __state = $state;
let __action_input = $action;
let __context = $context;
$crate::reducer_compose!(@accum __state, __action_input, __context; () $($arms)+)
}};
(@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) category $category:expr => $handler:expr, $($rest:tt)+) => {
$crate::reducer_compose!(
@accum $state, $action, $context;
(
$($out)*
__action if $crate::ActionCategory::category(&__action) == Some($category) => {
($handler)($state, __action)
},
)
$($rest)+
)
};
(@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) context $context_value:expr => $handler:expr, $($rest:tt)+) => {
$crate::reducer_compose!(
@accum $state, $action, $context;
(
$($out)*
__action if $context == $context_value => {
($handler)($state, __action)
},
)
$($rest)+
)
};
(@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) _ => $handler:expr, $($rest:tt)+) => {
$crate::reducer_compose!(
@accum $state, $action, $context;
(
$($out)*
__action => {
($handler)($state, __action)
},
)
$($rest)+
)
};
(@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) $pattern:pat $(if $guard:expr)? => $handler:expr, $($rest:tt)+) => {
$crate::reducer_compose!(
@accum $state, $action, $context;
(
$($out)*
__action @ $pattern $(if $guard)? => {
($handler)($state, __action)
},
)
$($rest)+
)
};
(@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) category $category:expr => $handler:expr $(,)?) => {
match $action {
$($out)*
__action if $crate::ActionCategory::category(&__action) == Some($category) => {
($handler)($state, __action)
}
}
};
(@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) context $context_value:expr => $handler:expr $(,)?) => {
match $action {
$($out)*
__action if $context == $context_value => {
($handler)($state, __action)
}
}
};
(@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) _ => $handler:expr $(,)?) => {
match $action {
$($out)*
__action => {
($handler)($state, __action)
}
}
};
(@accum $state:ident, $action:ident, $context:ident; ($($out:tt)*) $pattern:pat $(if $guard:expr)? => $handler:expr $(,)?) => {
match $action {
$($out)*
__action @ $pattern $(if $guard)? => {
($handler)($state, __action)
}
}
};
}
pub struct Store<S, A: Action, E = NoEffect> {
state: S,
reducer: Reducer<S, A, E>,
_marker: PhantomData<(A, E)>,
}
impl<S, A: Action, E> Store<S, A, E> {
pub fn new(state: S, reducer: Reducer<S, A, E>) -> Self {
Self {
state,
reducer,
_marker: PhantomData,
}
}
pub fn dispatch(&mut self, action: A) -> ReducerResult<E> {
(self.reducer)(&mut self.state, action)
}
pub fn state(&self) -> &S {
&self.state
}
pub fn state_mut(&mut self) -> &mut S {
&mut self.state
}
}
pub struct StoreWithMiddleware<S, A: Action, E = NoEffect, M = NoopMiddleware>
where
M: Middleware<S, A>,
{
store: Store<S, A, E>,
middleware: M,
dispatch_limits: DispatchLimits,
}
impl<S, A: Action, E, M: Middleware<S, A>> StoreWithMiddleware<S, A, E, M> {
pub fn new(state: S, reducer: Reducer<S, A, E>, middleware: M) -> Self {
Self {
store: Store::new(state, reducer),
middleware,
dispatch_limits: DispatchLimits::default(),
}
}
pub fn with_dispatch_limits(mut self, limits: DispatchLimits) -> Self {
debug_assert_valid_dispatch_limits(limits);
self.dispatch_limits = limits;
self
}
pub fn dispatch_limits(&self) -> DispatchLimits {
self.dispatch_limits
}
pub fn dispatch(&mut self, action: A) -> ReducerResult<E> {
self.try_dispatch(action)
.unwrap_or_else(|error| panic!("middleware dispatch failed: {error}"))
}
pub fn try_dispatch(&mut self, action: A) -> Result<ReducerResult<E>, DispatchError> {
let mut driver = StoreDispatchDriver {
store: &mut self.store,
middleware: &mut self.middleware,
};
run_iterative_middleware_dispatch(self.dispatch_limits, action, &mut driver)
}
pub fn state(&self) -> &S {
self.store.state()
}
pub fn state_mut(&mut self) -> &mut S {
self.store.state_mut()
}
pub fn middleware(&self) -> &M {
&self.middleware
}
pub fn middleware_mut(&mut self) -> &mut M {
&mut self.middleware
}
}
struct StoreDispatchDriver<'a, S, A: Action, E, M: Middleware<S, A>> {
store: &'a mut Store<S, A, E>,
middleware: &'a mut M,
}
impl<S, A: Action, E, M: Middleware<S, A>> MiddlewareDispatchDriver<A>
for StoreDispatchDriver<'_, S, A, E, M>
{
type Output = ReducerResult<E>;
fn before(&mut self, action: &A) -> bool {
self.middleware.before(action, &self.store.state)
}
fn reduce(&mut self, action: A) -> Self::Output {
self.store.dispatch(action)
}
fn cancelled_output(&mut self) -> Self::Output {
ReducerResult::unchanged()
}
fn after(&mut self, action: &A, result: &Self::Output) -> Vec<A> {
self.middleware
.after(action, result.changed, &self.store.state)
}
fn merge_child(&mut self, parent: &mut Self::Output, child: Self::Output) {
parent.changed |= child.changed;
parent.effects.extend(child.effects);
}
}
pub trait Middleware<S, A: Action> {
fn before(&mut self, action: &A, state: &S) -> bool;
fn after(&mut self, action: &A, state_changed: bool, state: &S) -> Vec<A>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopMiddleware;
impl<S, A: Action> Middleware<S, A> for NoopMiddleware {
fn before(&mut self, _action: &A, _state: &S) -> bool {
true
}
fn after(&mut self, _action: &A, _state_changed: bool, _state: &S) -> Vec<A> {
vec![]
}
}
#[cfg(feature = "tracing")]
#[derive(Debug, Clone, Default)]
pub struct LoggingMiddleware {
pub log_before: bool,
pub log_after: bool,
}
#[cfg(feature = "tracing")]
impl LoggingMiddleware {
pub fn new() -> Self {
Self {
log_before: false,
log_after: true,
}
}
pub fn verbose() -> Self {
Self {
log_before: true,
log_after: true,
}
}
}
#[cfg(feature = "tracing")]
impl<S, A: Action> Middleware<S, A> for LoggingMiddleware {
fn before(&mut self, action: &A, _state: &S) -> bool {
if self.log_before {
tracing::debug!(action = %action.name(), "Dispatching action");
}
true
}
fn after(&mut self, action: &A, state_changed: bool, _state: &S) -> Vec<A> {
if self.log_after {
tracing::debug!(
action = %action.name(),
state_changed = state_changed,
"Action processed"
);
}
vec![]
}
}
pub struct ComposedMiddleware<S, A: Action> {
middlewares: Vec<Box<dyn Middleware<S, A>>>,
}
impl<S, A: Action> std::fmt::Debug for ComposedMiddleware<S, A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ComposedMiddleware")
.field("middlewares_count", &self.middlewares.len())
.finish()
}
}
impl<S, A: Action> Default for ComposedMiddleware<S, A> {
fn default() -> Self {
Self::new()
}
}
impl<S, A: Action> ComposedMiddleware<S, A> {
pub fn new() -> Self {
Self {
middlewares: Vec::new(),
}
}
pub fn add<M: Middleware<S, A> + 'static>(&mut self, middleware: M) {
self.middlewares.push(Box::new(middleware));
}
}
impl<S, A: Action> Middleware<S, A> for ComposedMiddleware<S, A> {
fn before(&mut self, action: &A, state: &S) -> bool {
for middleware in &mut self.middlewares {
if !middleware.before(action, state) {
return false;
}
}
true
}
fn after(&mut self, action: &A, state_changed: bool, state: &S) -> Vec<A> {
let mut injected = Vec::new();
for middleware in self.middlewares.iter_mut().rev() {
injected.extend(middleware.after(action, state_changed, state));
}
injected
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ActionCategory;
#[derive(Default)]
struct TestState {
counter: i32,
}
#[derive(Clone, Debug)]
enum TestAction {
Increment,
Decrement,
NoOp,
}
impl Action for TestAction {
fn name(&self) -> &'static str {
match self {
TestAction::Increment => "Increment",
TestAction::Decrement => "Decrement",
TestAction::NoOp => "NoOp",
}
}
}
fn test_reducer(state: &mut TestState, action: TestAction) -> ReducerResult {
match action {
TestAction::Increment => {
state.counter += 1;
ReducerResult::changed()
}
TestAction::Decrement => {
state.counter -= 1;
ReducerResult::changed()
}
TestAction::NoOp => ReducerResult::unchanged(),
}
}
#[test]
fn test_store_dispatch() {
let mut store = Store::new(TestState::default(), test_reducer);
assert!(store.dispatch(TestAction::Increment).changed);
assert_eq!(store.state().counter, 1);
assert!(store.dispatch(TestAction::Increment).changed);
assert_eq!(store.state().counter, 2);
assert!(store.dispatch(TestAction::Decrement).changed);
assert_eq!(store.state().counter, 1);
}
#[test]
fn test_store_noop() {
let mut store = Store::new(TestState::default(), test_reducer);
assert!(!store.dispatch(TestAction::NoOp).changed);
assert_eq!(store.state().counter, 0);
}
#[test]
fn test_store_state_mut() {
let mut store = Store::new(TestState::default(), test_reducer);
store.state_mut().counter = 100;
assert_eq!(store.state().counter, 100);
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum TestEffect {
Log(String),
Save,
}
#[derive(Clone, Debug)]
enum EffectAction {
Decrement,
TriggerEffect,
}
impl Action for EffectAction {
fn name(&self) -> &'static str {
match self {
EffectAction::Decrement => "Decrement",
EffectAction::TriggerEffect => "TriggerEffect",
}
}
}
fn effect_reducer(state: &mut TestState, action: EffectAction) -> ReducerResult<TestEffect> {
match action {
EffectAction::Decrement => {
state.counter -= 1;
ReducerResult::changed_with(TestEffect::Log(format!("count: {}", state.counter)))
}
EffectAction::TriggerEffect => {
ReducerResult::effects(vec![TestEffect::Log("triggered".into()), TestEffect::Save])
}
}
}
#[test]
fn reducer_result_builders_preserve_changed_and_effects() {
let r: ReducerResult<TestEffect> = ReducerResult::unchanged();
assert!(!r.changed);
assert!(r.effects.is_empty());
let r: ReducerResult<TestEffect> = ReducerResult::changed();
assert!(r.changed);
assert!(r.effects.is_empty());
let r = ReducerResult::effect(TestEffect::Save);
assert!(!r.changed);
assert_eq!(r.effects, vec![TestEffect::Save]);
let r = ReducerResult::changed_with(TestEffect::Save);
assert!(r.changed);
assert_eq!(r.effects, vec![TestEffect::Save]);
let r =
ReducerResult::changed_with_many(vec![TestEffect::Save, TestEffect::Log("x".into())]);
assert!(r.changed);
assert_eq!(r.effects.len(), 2);
}
#[test]
fn reducer_result_chaining_can_add_effect_and_mark_changed() {
let r: ReducerResult<TestEffect> = ReducerResult::unchanged()
.with(TestEffect::Save)
.mark_changed();
assert!(r.changed);
assert_eq!(r.effects, vec![TestEffect::Save]);
}
#[test]
fn store_dispatch_supports_effect_reducer_results() {
let mut store = Store::new(TestState::default(), effect_reducer);
let result = store.dispatch(EffectAction::Decrement);
assert!(result.changed);
assert_eq!(result.effects, vec![TestEffect::Log("count: -1".into())]);
let result = store.dispatch(EffectAction::TriggerEffect);
assert!(!result.changed);
assert_eq!(
result.effects,
vec![TestEffect::Log("triggered".into()), TestEffect::Save]
);
}
#[derive(Default)]
struct CountingMiddleware {
before_count: usize,
after_count: usize,
}
impl<S, A: Action> Middleware<S, A> for CountingMiddleware {
fn before(&mut self, _action: &A, _state: &S) -> bool {
self.before_count += 1;
true
}
fn after(&mut self, _action: &A, _state_changed: bool, _state: &S) -> Vec<A> {
self.after_count += 1;
vec![]
}
}
#[test]
fn test_store_with_middleware() {
let mut store = StoreWithMiddleware::new(
TestState::default(),
test_reducer,
CountingMiddleware::default(),
);
store.dispatch(TestAction::Increment);
store.dispatch(TestAction::Increment);
assert_eq!(store.middleware().before_count, 2);
assert_eq!(store.middleware().after_count, 2);
assert_eq!(store.state().counter, 2);
}
struct SelfInjectingMiddleware;
impl Middleware<TestState, TestAction> for SelfInjectingMiddleware {
fn before(&mut self, _action: &TestAction, _state: &TestState) -> bool {
true
}
fn after(
&mut self,
action: &TestAction,
_state_changed: bool,
_state: &TestState,
) -> Vec<TestAction> {
vec![action.clone()]
}
}
#[test]
fn test_try_dispatch_depth_exceeded() {
let mut store =
StoreWithMiddleware::new(TestState::default(), test_reducer, SelfInjectingMiddleware)
.with_dispatch_limits(DispatchLimits {
max_depth: 2,
max_actions: 100,
});
let err = store.try_dispatch(TestAction::Increment).unwrap_err();
assert_eq!(
err,
DispatchError::DepthExceeded {
max_depth: 2,
action: "Increment",
}
);
assert_eq!(store.state().counter, 2);
}
#[test]
fn test_try_dispatch_action_budget_exceeded() {
let mut store =
StoreWithMiddleware::new(TestState::default(), test_reducer, SelfInjectingMiddleware)
.with_dispatch_limits(DispatchLimits {
max_depth: 32,
max_actions: 2,
});
let err = store.try_dispatch(TestAction::Increment).unwrap_err();
assert_eq!(
err,
DispatchError::ActionBudgetExceeded {
max_actions: 2,
processed: 2,
action: "Increment",
}
);
assert_eq!(store.state().counter, 2);
}
struct FiniteCascadeMiddleware {
target: i32,
}
impl Middleware<TestState, TestAction> for FiniteCascadeMiddleware {
fn before(&mut self, _action: &TestAction, _state: &TestState) -> bool {
true
}
fn after(
&mut self,
action: &TestAction,
_state_changed: bool,
state: &TestState,
) -> Vec<TestAction> {
if matches!(action, TestAction::Increment) && state.counter < self.target {
vec![TestAction::Increment]
} else {
vec![]
}
}
}
#[test]
fn test_try_dispatch_deep_finite_chain_succeeds() {
let target = 512usize;
let mut store = StoreWithMiddleware::new(
TestState::default(),
test_reducer,
FiniteCascadeMiddleware {
target: target as i32,
},
)
.with_dispatch_limits(DispatchLimits {
max_depth: target + 1,
max_actions: target + 1,
});
let result = store.try_dispatch(TestAction::Increment).unwrap();
assert!(result.changed);
assert_eq!(store.state().counter, target as i32);
}
#[derive(Default)]
struct OrderingState {
order: Vec<&'static str>,
}
#[derive(Clone, Debug)]
enum OrderingAction {
Root,
Left,
Right,
Leaf,
}
impl Action for OrderingAction {
fn name(&self) -> &'static str {
match self {
OrderingAction::Root => "Root",
OrderingAction::Left => "Left",
OrderingAction::Right => "Right",
OrderingAction::Leaf => "Leaf",
}
}
}
fn ordering_reducer(state: &mut OrderingState, action: OrderingAction) -> ReducerResult {
state.order.push(action.name());
ReducerResult::changed()
}
struct OrderingMiddleware;
impl Middleware<OrderingState, OrderingAction> for OrderingMiddleware {
fn before(&mut self, _action: &OrderingAction, _state: &OrderingState) -> bool {
true
}
fn after(
&mut self,
action: &OrderingAction,
_state_changed: bool,
_state: &OrderingState,
) -> Vec<OrderingAction> {
match action {
OrderingAction::Root => vec![OrderingAction::Left, OrderingAction::Right],
OrderingAction::Left => vec![OrderingAction::Leaf],
OrderingAction::Right | OrderingAction::Leaf => vec![],
}
}
}
#[test]
fn test_try_dispatch_injection_order_is_depth_first() {
let mut store = StoreWithMiddleware::new(
OrderingState::default(),
ordering_reducer,
OrderingMiddleware,
)
.with_dispatch_limits(DispatchLimits {
max_depth: 8,
max_actions: 8,
});
let result = store.try_dispatch(OrderingAction::Root).unwrap();
assert!(result.changed);
assert_eq!(store.state().order, vec!["Root", "Left", "Leaf", "Right"]);
}
fn ordering_effect_reducer(
state: &mut OrderingState,
action: OrderingAction,
) -> ReducerResult<TestEffect> {
state.order.push(action.name());
ReducerResult::changed_with(TestEffect::Log(action.name().into()))
}
#[test]
fn test_try_dispatch_merges_child_effects_in_depth_first_order() {
let mut store = StoreWithMiddleware::new(
OrderingState::default(),
ordering_effect_reducer,
OrderingMiddleware,
)
.with_dispatch_limits(DispatchLimits {
max_depth: 8,
max_actions: 8,
});
let result = store.try_dispatch(OrderingAction::Root).unwrap();
assert!(result.changed);
assert_eq!(
result.effects,
vec![
TestEffect::Log("Root".into()),
TestEffect::Log("Left".into()),
TestEffect::Log("Leaf".into()),
TestEffect::Log("Right".into()),
]
);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum ComposeContext {
Default,
Command,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum ComposeCategory {
Nav,
Search,
Uncategorized,
}
#[derive(Clone, Debug)]
enum ComposeAction {
NavUp,
Search,
Other,
}
impl Action for ComposeAction {
fn name(&self) -> &'static str {
match self {
ComposeAction::NavUp => "NavUp",
ComposeAction::Search => "Search",
ComposeAction::Other => "Other",
}
}
}
impl ActionCategory for ComposeAction {
type Category = ComposeCategory;
fn category(&self) -> Option<&'static str> {
match self {
ComposeAction::NavUp => Some("nav"),
ComposeAction::Search => Some("search"),
ComposeAction::Other => None,
}
}
fn category_enum(&self) -> Self::Category {
match self {
ComposeAction::NavUp => ComposeCategory::Nav,
ComposeAction::Search => ComposeCategory::Search,
ComposeAction::Other => ComposeCategory::Uncategorized,
}
}
}
fn handle_nav(state: &mut usize, _action: ComposeAction) -> &'static str {
*state += 1;
"nav"
}
fn handle_command(state: &mut usize, _action: ComposeAction) -> &'static str {
*state += 10;
"command"
}
fn handle_search(state: &mut usize, _action: ComposeAction) -> &'static str {
*state += 100;
"search"
}
fn handle_default(state: &mut usize, _action: ComposeAction) -> &'static str {
*state += 1000;
"default"
}
fn composed_reducer(
state: &mut usize,
action: ComposeAction,
context: ComposeContext,
) -> &'static str {
crate::reducer_compose!(state, action, context, {
category "nav" => handle_nav,
context ComposeContext::Command => handle_command,
ComposeAction::Search => handle_search,
_ => handle_default,
})
}
#[test]
fn test_reducer_compose_routes_category() {
let mut state = 0;
let result = composed_reducer(&mut state, ComposeAction::NavUp, ComposeContext::Command);
assert_eq!(result, "nav");
assert_eq!(state, 1);
}
#[test]
fn test_reducer_compose_routes_context() {
let mut state = 0;
let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Command);
assert_eq!(result, "command");
assert_eq!(state, 10);
}
#[test]
fn test_reducer_compose_routes_pattern() {
let mut state = 0;
let result = composed_reducer(&mut state, ComposeAction::Search, ComposeContext::Default);
assert_eq!(result, "search");
assert_eq!(state, 100);
}
#[test]
fn test_reducer_compose_routes_fallback() {
let mut state = 0;
let result = composed_reducer(&mut state, ComposeAction::Other, ComposeContext::Default);
assert_eq!(result, "default");
assert_eq!(state, 1000);
}
fn composed_reducer_no_context(state: &mut usize, action: ComposeAction) -> &'static str {
crate::reducer_compose!(state, action, {
category "nav" => handle_nav,
ComposeAction::Search => handle_search,
_ => handle_default,
})
}
#[test]
fn test_reducer_compose_3arg_category() {
let mut state = 0;
let result = composed_reducer_no_context(&mut state, ComposeAction::NavUp);
assert_eq!(result, "nav");
assert_eq!(state, 1);
}
#[test]
fn test_reducer_compose_3arg_pattern() {
let mut state = 0;
let result = composed_reducer_no_context(&mut state, ComposeAction::Search);
assert_eq!(result, "search");
assert_eq!(state, 100);
}
#[test]
fn test_reducer_compose_3arg_fallback() {
let mut state = 0;
let result = composed_reducer_no_context(&mut state, ComposeAction::Other);
assert_eq!(result, "default");
assert_eq!(state, 1000);
}
}