├── .gitignore ├── .travis.yml ├── Cargo.toml ├── README.md ├── examples └── simple.rs ├── src └── lib.rs └── tests └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | doc 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redux" 3 | version = "0.0.1" 4 | authors = ["Jared McFarland "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-rs 2 | 3 | ![travis-ci](https://travis-ci.org/jaredonline/redux-rs.svg) 4 | 5 | An attempt at a uni-directional state flow written in Rust, heavily based in [redux-js](http://redux.js.org/). 6 | 7 | ## Usage 8 | 9 | Here's a simple example of using a store and reducer to make a quick Todo list (you can run this by running `cargo run --example simple` or view the code [here](https://github.com/jaredonline/redux-rs/blob/master/examples/simple.rs)). 10 | 11 | ```rust 12 | extern crate redux; 13 | use redux::{Store, Reducer}; 14 | use std::default::Default; 15 | 16 | #[derive(Clone, Debug)] 17 | struct Todo { 18 | name: &'static str, 19 | } 20 | 21 | #[derive(Clone, Debug)] 22 | struct TodoState { 23 | todos: Vec, 24 | } 25 | 26 | impl TodoState { 27 | fn new() -> TodoState { 28 | TodoState { 29 | todos: vec![], 30 | } 31 | } 32 | 33 | fn push(&mut self, todo: Todo) { 34 | self.todos.push(todo); 35 | } 36 | } 37 | 38 | #[derive(Clone)] 39 | enum TodoAction { 40 | Insert(&'static str), 41 | } 42 | 43 | impl Default for TodoState { 44 | fn default() -> Self { 45 | TodoState::new() 46 | } 47 | } 48 | 49 | impl Reducer for TodoState { 50 | type Action = TodoAction; 51 | type Error = String; 52 | 53 | fn reduce(&mut self, action: Self::Action) -> Result { 54 | match action { 55 | TodoAction::Insert(name) => { 56 | let todo = Todo { name: name, }; 57 | self.push(todo); 58 | }, 59 | } 60 | 61 | Ok(self.clone()) 62 | } 63 | } 64 | 65 | fn main() { 66 | let store : Store = Store::new(vec![]); 67 | let action = TodoAction::Insert("Clean the bathroom"); 68 | let _ = store.dispatch(action); 69 | 70 | println!("{:?}", store.get_state()); 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | extern crate redux; 2 | use redux::{Store, Reducer}; 3 | use std::default::Default; 4 | 5 | #[derive(Clone, Debug)] 6 | struct Todo { 7 | name: &'static str, 8 | } 9 | 10 | #[derive(Clone, Debug)] 11 | struct TodoState { 12 | todos: Vec, 13 | } 14 | 15 | impl TodoState { 16 | fn new() -> TodoState { 17 | TodoState { 18 | todos: vec![], 19 | } 20 | } 21 | 22 | fn push(&mut self, todo: Todo) { 23 | self.todos.push(todo); 24 | } 25 | } 26 | 27 | #[derive(Clone)] 28 | enum TodoAction { 29 | Insert(&'static str), 30 | } 31 | 32 | impl Default for TodoState { 33 | fn default() -> Self { 34 | TodoState::new() 35 | } 36 | } 37 | 38 | impl Reducer for TodoState { 39 | type Action = TodoAction; 40 | type Error = String; 41 | 42 | fn reduce(&mut self, action: Self::Action) -> Result { 43 | match action { 44 | TodoAction::Insert(name) => { 45 | let todo = Todo { name: name, }; 46 | self.push(todo); 47 | }, 48 | } 49 | 50 | Ok(self.clone()) 51 | } 52 | } 53 | 54 | fn main() { 55 | let store : Store = Store::new(vec![]); 56 | let action = TodoAction::Insert("Clean the bathroom"); 57 | let _ = store.dispatch(action); 58 | 59 | println!("{:?}", store.get_state()); 60 | } 61 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex, RwLock}; 2 | use std::default::Default; 3 | use std::fmt::Display; 4 | 5 | /// The `Reducer` trait is meant to be applied to the object that contains your 6 | /// applications state. Because each application will have their own custom state 7 | /// to track, we don't provide a sort of state object in redux-rs. 8 | /// 9 | /// redux-rs expects a 1:1:1 mapping between your Store, your State and your Reducer 10 | /// 11 | /// ## Types 12 | /// 13 | /// `Reducer` requires you provide two types: 14 | /// - `Action` is the type of action your `Reducer` reduces 15 | /// - `Error` the type of error this `Reducer` can return 16 | /// 17 | /// ## Required traits 18 | /// 19 | /// `Reducer` requires your type implements `Clone` and `Default`. 20 | /// 21 | /// ## Example 22 | /// 23 | /// Here's an example that provides a state object, implements Reducer on it and 24 | /// creates the store: 25 | /// 26 | /// ``` 27 | /// # #[allow(dead_code)] 28 | /// use redux::{Reducer, Store}; 29 | /// 30 | /// #[derive(Clone, Default)] 31 | /// struct MyState { 32 | /// foo: usize, 33 | /// bar: usize, 34 | /// } 35 | /// 36 | /// impl Reducer for MyState { 37 | /// type Action = String; 38 | /// type Error = String; 39 | /// 40 | /// fn reduce(&mut self, action: Self::Action) -> Result { 41 | /// Ok(self.clone()) 42 | /// } 43 | /// } 44 | /// 45 | /// fn main() { 46 | /// let store : Store = Store::new(vec![]); 47 | /// } 48 | /// ``` 49 | pub trait Reducer: Clone + Default { 50 | /// The type of action that this reducer can accept, probably an enum 51 | type Action: Clone; 52 | 53 | /// The type of error this reducer can return in the `Result` 54 | type Error: Display; 55 | 56 | /// Reduce a given state based upon an action. This won't be called externally 57 | /// because your application will never have a reference to the state object 58 | /// directly. Instead, it'll be called with you call `store.dispatch`. 59 | fn reduce(&mut self, Self::Action) -> Result; 60 | } 61 | 62 | fn build_next(next: DispatchFunc, middleware: Box>) -> DispatchFunc { 63 | Box::new(move |store, action| { 64 | middleware.dispatch(store, action, &next) 65 | }) 66 | } 67 | 68 | /// The `Store` is the main access point for your application. As soon as you 69 | /// initialize your `Store` it will start your state in the default state and 70 | /// allow you to start dispatching events to it. 71 | /// 72 | /// ## Example 73 | /// 74 | /// ``` 75 | /// # #[allow(dead_code)] 76 | /// use redux::{Reducer, Store}; 77 | /// 78 | /// #[derive(Clone, Debug)] 79 | /// struct Todo { 80 | /// name: &'static str, 81 | /// } 82 | /// 83 | /// #[derive(Clone, Debug)] 84 | /// struct TodoState { 85 | /// todos: Vec, 86 | /// } 87 | /// 88 | /// impl TodoState { 89 | /// fn new() -> TodoState { 90 | /// TodoState { 91 | /// todos: vec![], 92 | /// } 93 | /// } 94 | /// 95 | /// fn push(&mut self, todo: Todo) { 96 | /// self.todos.push(todo); 97 | /// } 98 | /// } 99 | /// 100 | /// #[derive(Clone)] 101 | /// enum TodoAction { 102 | /// Insert(&'static str), 103 | /// } 104 | /// 105 | /// impl Default for TodoState { 106 | /// fn default() -> Self { 107 | /// TodoState::new() 108 | /// } 109 | /// } 110 | /// 111 | /// impl Reducer for TodoState { 112 | /// type Action = TodoAction; 113 | /// type Error = String; 114 | /// 115 | /// fn reduce(&mut self, action: Self::Action) -> Result { 116 | /// match action { 117 | /// TodoAction::Insert(name) => { 118 | /// let todo = Todo { name: name, }; 119 | /// self.push(todo); 120 | /// }, 121 | /// } 122 | /// 123 | /// Ok(self.clone()) 124 | /// } 125 | /// } 126 | /// 127 | /// fn main() { 128 | /// let store : Store = Store::new(vec![]); 129 | /// let action = TodoAction::Insert("Clean the bathroom"); 130 | /// let _ = store.dispatch(action); 131 | /// 132 | /// println!("{:?}", store.get_state()); 133 | /// } 134 | /// ``` 135 | pub struct Store { 136 | internal_store: Arc>>, 137 | subscriptions: Arc>>>>, 138 | dispatch_chain: DispatchFunc, 139 | } 140 | 141 | // Would love to get rid of these someday 142 | unsafe impl Send for Store {} 143 | unsafe impl Sync for Store {} 144 | 145 | impl Store { 146 | /// Initialize a new `Store`. 147 | pub fn new(middlewares: Vec>>) -> Store { 148 | let initial_data = T::default(); 149 | let internal = Arc::new(Mutex::new(InternalStore { 150 | data: initial_data, 151 | is_dispatching: false, 152 | })); 153 | let is = internal.clone(); 154 | let mut next : DispatchFunc = Box::new(move |_, action| { 155 | match is.try_lock() { 156 | Ok(mut guard) => { 157 | guard.dispatch(action.clone()) 158 | }, 159 | Err(_) => { 160 | Err(String::from("Can't dispatch during a reduce. The internal data is locked.")) 161 | } 162 | } 163 | }); 164 | for middleware in middlewares { 165 | next = build_next(next, middleware); 166 | } 167 | 168 | Store { 169 | internal_store: internal, 170 | subscriptions: Arc::new(RwLock::new(Vec::new())), 171 | dispatch_chain: next, 172 | } 173 | } 174 | 175 | /// Dispatch an event to the stores, returning an `Result`. Only one dispatch 176 | /// can be happening at a time. 177 | pub fn dispatch(&self, action: T::Action) -> Result { 178 | let ref dispatch = self.dispatch_chain; 179 | match dispatch(&self, action.clone()) { 180 | Err(e) => return Err(format!("Error during dispatch: {}", e)), 181 | _ => {} 182 | } 183 | 184 | // snapshot the active subscriptions here before calling them. This both 185 | // emulates the Redux.js way of doing them *and* frees up the lock so 186 | // that a subscription can cause another subscription; also use this 187 | // loop to grab the ones that are safe to remove and try to remove them 188 | // after this 189 | let (subs_to_remove, subs_to_use) = self.get_subscriptions(); 190 | 191 | // on every subscription callback loop we gather the indexes of cancelled 192 | // subscriptions; if we leave a loop and have cancelled subscriptions, we'll 193 | // try to remove them here 194 | self.try_to_remove_subscriptions(subs_to_remove); 195 | 196 | // actually run the subscriptions here; after this method is over the subs_to_use 197 | // vec gets dropped, and all the Arcs of subscriptions get decremented 198 | for subscription in subs_to_use { 199 | let cb = &subscription.callback; 200 | cb(&self, &subscription); 201 | } 202 | 203 | Ok(action) 204 | } 205 | 206 | /// Returns a `Clone` of the store's state. If called during a dispatch, this 207 | /// will block until the dispatch is over. 208 | pub fn get_state(&self) -> T { 209 | self.internal_store.lock().unwrap().data.clone() 210 | } 211 | 212 | /// Create a new subscription to this store. Subscriptions are called for every 213 | /// dispatch made. 214 | /// 215 | /// ## Nested subscriptions 216 | /// 217 | /// Its possible to subscribe to a store from within a currently called 218 | /// subscription: 219 | /// 220 | /// ``` 221 | /// # #[allow(dead_code)] 222 | /// # use redux::{Reducer, Store}; 223 | /// # 224 | /// # #[derive(Clone, Default)] 225 | /// # struct Foo {} 226 | /// # impl Reducer for Foo { 227 | /// # type Action = usize; 228 | /// # type Error = String; 229 | /// # 230 | /// # fn reduce(&mut self, _: Self::Action) -> Result { 231 | /// # Ok(self.clone()) 232 | /// # } 233 | /// # } 234 | /// # 235 | /// # let store : Store = Store::new(vec![]); 236 | /// store.subscribe(Box::new(|store, _| { 237 | /// store.subscribe(Box::new(|_, _| { })); 238 | /// })); 239 | /// ``` 240 | /// 241 | /// The nested subscription won't be called until the next dispatch. 242 | /// 243 | /// ## Snapshotting subscriptions 244 | /// 245 | /// Subscriptions are snap-shotted immediately after the reducer and middlewares 246 | /// finish and before the subscriptions are called, so any subscriptions made 247 | /// during a subscription callback won't be fired until the next dispatch 248 | /// 249 | /// ## Return value 250 | /// 251 | /// This method returns a `Subscription` wrapped in an `Arc` because both 252 | /// the caller of the method and the internal list of subscriptions need 253 | /// a reference to it 254 | pub fn subscribe(&self, callback: SubscriptionFunc) -> Arc> { 255 | let subscription = Arc::new(Subscription::new(callback)); 256 | let s = subscription.clone(); 257 | self.subscriptions.write().unwrap().push(s); 258 | return subscription; 259 | } 260 | 261 | fn get_subscriptions(&self) -> (Vec, Vec>>) { 262 | let mut i = 0; 263 | let mut subs_to_remove = vec![]; 264 | let mut subs_to_use = vec![]; 265 | { 266 | let subscriptions = self.subscriptions.read().unwrap(); 267 | for subscription in &(*subscriptions) { 268 | if subscription.is_active() { 269 | subs_to_use.push(subscription.clone()); 270 | } else { 271 | subs_to_remove.push(i); 272 | } 273 | i += 1; 274 | } 275 | } 276 | 277 | (subs_to_remove, subs_to_use) 278 | } 279 | 280 | fn try_to_remove_subscriptions(&self, subs_to_remove: Vec) { 281 | if subs_to_remove.len() > 0 { 282 | match self.subscriptions.try_write() { 283 | Ok(mut subscriptions) => { 284 | for sub_index in subs_to_remove { 285 | subscriptions.remove(sub_index); 286 | } 287 | }, 288 | _ => {} 289 | } 290 | } 291 | } 292 | } 293 | 294 | struct InternalStore { 295 | data: T, 296 | is_dispatching: bool, 297 | } 298 | 299 | impl InternalStore { 300 | fn dispatch(&mut self, action: T::Action) -> Result { 301 | if self.is_dispatching { 302 | return Err(String::from("Can't dispatch during a reduce.")); 303 | } 304 | 305 | self.is_dispatching = true; 306 | match self.data.reduce(action.clone()) { 307 | Ok(_) => {} 308 | Err(e) => { 309 | return Err(format!("{}", e)); 310 | } 311 | } 312 | self.is_dispatching = false; 313 | 314 | Ok(self.data.clone()) 315 | } 316 | } 317 | 318 | type SubscriptionFunc = Box, &Subscription)>; 319 | 320 | /// Represents a subscription to a `Store` which can be cancelled. 321 | pub struct Subscription { 322 | callback: SubscriptionFunc, 323 | active: Mutex, 324 | } 325 | 326 | unsafe impl Send for Subscription {} 327 | unsafe impl Sync for Subscription {} 328 | 329 | impl Subscription { 330 | fn new(callback: SubscriptionFunc) -> Subscription { 331 | Subscription { 332 | callback: callback, 333 | active: Mutex::new(true), 334 | } 335 | } 336 | 337 | /// Cancels a subscription which means it will no longer be called on a 338 | /// dispatch and it will be removed from the internal list of subscriptions 339 | /// at the next available time. 340 | /// 341 | /// A cancelled subscription cannot be re-instated 342 | pub fn cancel(&self) { 343 | let mut active = self.active.lock().unwrap(); 344 | *active = false; 345 | } 346 | 347 | /// Returns whether or not a subscription has been cancelled. 348 | pub fn is_active(&self) -> bool { 349 | *self.active.lock().unwrap() 350 | } 351 | } 352 | 353 | pub type DispatchFunc = Box, T::Action) -> Result>; 354 | 355 | /// A decent approximation of a redux-js middleware wrapper. This lets you have 356 | /// wrap calls to dispatch, performing actions right before and right after a 357 | /// call. Each call to dispatch in a Store will loop the middlewares, calling 358 | /// before, then call the dispatch, then loop the middlewares in reverse order 359 | /// calling after. 360 | /// 361 | /// ## Example: 362 | /// 363 | /// ``` 364 | /// # #[allow(dead_code)] 365 | /// # use redux::{Store, Reducer, Middleware, DispatchFunc}; 366 | /// # 367 | /// # #[derive(Clone, Debug)] 368 | /// # enum FooAction {} 369 | /// # 370 | /// # #[derive(Clone, Default, Debug)] 371 | /// # struct Foo {} 372 | /// # impl Reducer for Foo { 373 | /// # type Action = FooAction; 374 | /// # type Error = String; 375 | /// # 376 | /// # fn reduce(&mut self, _: Self::Action) -> Result { 377 | /// # Ok(self.clone()) 378 | /// # } 379 | /// # } 380 | /// 381 | /// struct Logger{} 382 | /// impl Middleware for Logger { 383 | /// fn dispatch(&self, store: &Store, action: FooAction, next: &DispatchFunc) -> Result { 384 | /// println!("Called action: {:?}", action); 385 | /// println!("State before action: {:?}", store.get_state()); 386 | /// let result = next(store, action); 387 | /// println!("State after action: {:?}", store.get_state()); 388 | /// 389 | /// result 390 | /// } 391 | /// } 392 | /// 393 | /// let logger = Box::new(Logger{}); 394 | /// let store : Store = Store::new(vec![logger]); 395 | /// ``` 396 | pub trait Middleware { 397 | fn dispatch(&self, store: &Store, action: T::Action, next: &DispatchFunc) -> Result; 398 | } 399 | 400 | #[cfg(test)] 401 | impl Reducer for usize { 402 | type Action = usize; 403 | type Error = String; 404 | 405 | fn reduce(&mut self, _: Self::Action) -> Result { 406 | Ok(self.clone()) 407 | } 408 | } 409 | 410 | #[test] 411 | fn get_subscriptions() { 412 | let store : Store = Store::new(vec![]); 413 | { 414 | let (remove, subs) = store.get_subscriptions(); 415 | assert_eq!(0, remove.len()); 416 | assert_eq!(0, subs.len()); 417 | } 418 | 419 | let sub = store.subscribe(Box::new(|_, _| {})); 420 | { 421 | let (remove, subs) = store.get_subscriptions(); 422 | assert_eq!(0, remove.len()); 423 | assert_eq!(1, subs.len()); 424 | } 425 | 426 | sub.cancel(); 427 | { 428 | let (remove, subs) = store.get_subscriptions(); 429 | assert_eq!(1, remove.len()); 430 | assert_eq!(0, subs.len()); 431 | } 432 | } 433 | 434 | #[test] 435 | fn try_remove_subscriptions_easy_lock() { 436 | let store : Store = Store::new(vec![]); 437 | let sub = store.subscribe(Box::new(|_, _| {})); 438 | sub.cancel(); 439 | 440 | let (remove, _) = store.get_subscriptions(); 441 | store.try_to_remove_subscriptions(remove); 442 | let (_, subs) = store.get_subscriptions(); 443 | assert_eq!(0, subs.len()); 444 | assert_eq!(0, store.subscriptions.read().unwrap().len()); 445 | } 446 | 447 | #[test] 448 | fn try_remove_subscriptions_no_lock() { 449 | let store : Store = Store::new(vec![]); 450 | let sub = store.subscribe(Box::new(|_, _| {})); 451 | sub.cancel(); 452 | 453 | let (remove, _) = store.get_subscriptions(); 454 | { 455 | let subscriptions = store.subscriptions.write().unwrap(); 456 | store.try_to_remove_subscriptions(remove); 457 | } 458 | assert_eq!(1, store.subscriptions.read().unwrap().len()); 459 | } 460 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate redux; 2 | 3 | use redux::{Reducer, Store, Middleware, DispatchFunc}; 4 | 5 | use std::collections::HashMap; 6 | use std::sync::{Mutex, Arc}; 7 | use std::{thread, time}; 8 | use std::default::Default; 9 | 10 | #[derive(Clone)] 11 | enum TodoAction { 12 | NewTodo { name: String } 13 | } 14 | 15 | #[derive(Clone)] 16 | struct Todo { 17 | name: String, 18 | id: usize, 19 | } 20 | 21 | #[derive(Clone)] 22 | struct TodoStore { 23 | todos: HashMap, 24 | vec: Vec, 25 | ticket: usize, 26 | } 27 | 28 | impl TodoStore { 29 | pub fn new() -> TodoStore { 30 | TodoStore { 31 | todos: HashMap::new(), 32 | vec: Vec::new(), 33 | ticket: 0, 34 | } 35 | } 36 | 37 | pub fn ticket(&mut self) -> usize { 38 | self.ticket += 1; 39 | self.ticket 40 | } 41 | 42 | pub fn push(&mut self, todo: Todo) { 43 | let ticket = todo.id; 44 | self.todos.insert(ticket, todo); 45 | self.vec.push(ticket); 46 | } 47 | 48 | pub fn len(&self) -> usize { 49 | self.vec.len() 50 | } 51 | } 52 | 53 | impl Default for TodoStore { 54 | fn default() -> Self { 55 | TodoStore::new() 56 | } 57 | } 58 | 59 | impl Reducer for TodoStore { 60 | type Action = TodoAction; 61 | type Error = String; 62 | 63 | fn reduce(&mut self, action: Self::Action) -> Result { 64 | match action { 65 | TodoAction::NewTodo { name } => { 66 | let todo = Todo { name: name, id: self.ticket(), }; 67 | self.push(todo); 68 | }, 69 | // _ => {} 70 | } 71 | 72 | Ok(self.clone()) 73 | } 74 | } 75 | 76 | #[test] 77 | fn todo_list() { 78 | struct PingbackTester { 79 | counter: usize 80 | } 81 | let pingbacker = Arc::new(Mutex::new(PingbackTester { counter: 0 })); 82 | 83 | let store : Store = Store::new(vec![]); 84 | let pbacker = pingbacker.clone(); 85 | store.subscribe(Box::new(move |_, _| { 86 | let mut pingbacker = pingbacker.lock().unwrap(); 87 | pingbacker.counter += 1; 88 | })); 89 | 90 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 91 | let _ = store.dispatch(action); 92 | assert_eq!(1, store.get_state().len()); 93 | assert_eq!(1, pbacker.lock().unwrap().counter); 94 | } 95 | 96 | #[test] 97 | fn dispatch_from_a_listener() { 98 | let store : Store = Store::new(vec![]); 99 | store.subscribe(Box::new(|store, _| { 100 | if store.get_state().len() < 2 { 101 | let action = TodoAction::NewTodo {name: String::from("Finish that new todo")}; 102 | let _ = store.dispatch(action); 103 | } 104 | })); 105 | 106 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 107 | let _ = store.dispatch(action); 108 | assert_eq!(2, store.get_state().len()); 109 | } 110 | 111 | #[test] 112 | fn multi_threaded_use() { 113 | let mut store : Arc> = Arc::new(Store::new(vec![])); 114 | { 115 | let store = Arc::get_mut(&mut store).unwrap(); 116 | store.subscribe(Box::new(|s, _| { 117 | if s.get_state().len() < 2 { 118 | let action = TodoAction::NewTodo {name: String::from("Add-on to g-shopping")}; 119 | let _ = s.dispatch(action); 120 | } 121 | })); 122 | } 123 | let s = store.clone(); 124 | thread::spawn(move || { 125 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 126 | let _ = s.dispatch(action); 127 | }); 128 | 129 | thread::sleep(time::Duration::from_secs(1)); 130 | 131 | assert_eq!(2, store.get_state().len()); 132 | } 133 | 134 | #[test] 135 | fn cancel_subscription() { 136 | struct PingbackTester { 137 | counter: usize 138 | } 139 | let pingbacker = Arc::new(Mutex::new(PingbackTester { counter: 0 })); 140 | 141 | let store : Store = Store::new(vec![]); 142 | let pbacker = pingbacker.clone(); 143 | let subscription = store.subscribe(Box::new(move |_, _| { 144 | let mut pingbacker = pingbacker.lock().unwrap(); 145 | pingbacker.counter += 1; 146 | })); 147 | 148 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 149 | let _ = store.dispatch(action); 150 | assert_eq!(1, store.get_state().len()); 151 | assert_eq!(1, pbacker.lock().unwrap().counter); 152 | 153 | subscription.cancel(); 154 | let action2 = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 155 | let _ = store.dispatch(action2); 156 | assert_eq!(2, store.get_state().len()); 157 | assert_eq!(1, pbacker.lock().unwrap().counter); 158 | } 159 | 160 | struct Counter { 161 | before_count: Arc>, 162 | after_count: Arc>, 163 | } 164 | impl Counter { 165 | fn new(before_count: Arc>, after_count: Arc>) -> Counter { 166 | Counter { 167 | before_count: before_count, 168 | after_count: after_count, 169 | } 170 | } 171 | } 172 | impl Middleware for Counter { 173 | fn dispatch(&self, store: &Store, action: TodoAction, next: &DispatchFunc) -> Result { 174 | let mut count = self.before_count.lock().unwrap(); 175 | *count += 1; 176 | let result = next(store, action); 177 | let mut count = self.after_count.lock().unwrap(); 178 | *count += 2; 179 | 180 | result 181 | } 182 | } 183 | 184 | #[test] 185 | fn middleware() { 186 | let before_count = Arc::new(Mutex::new(0)); 187 | let after_count = Arc::new(Mutex::new(0)); 188 | let counter = Box::new(Counter::new(before_count.clone(), after_count.clone())); 189 | let store : Store = Store::new(vec![counter]); 190 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 191 | let _ = store.dispatch(action); 192 | assert_eq!(1, store.get_state().len()); 193 | assert_eq!(1, *before_count.lock().unwrap()); 194 | assert_eq!(2, *after_count.lock().unwrap()); 195 | } 196 | 197 | #[test] 198 | fn subscribe_during_subscription_callback() { 199 | let store : Store = Store::new(vec![]); 200 | 201 | // on our first action, sub another subscriber that adds more actions 202 | let sub = store.subscribe(Box::new(move |store, _| { 203 | store.subscribe(Box::new(|store, _| { 204 | if store.get_state().len() < 5 { 205 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 206 | let _ = store.dispatch(action); 207 | } 208 | })); 209 | })); 210 | 211 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 212 | let _ = store.dispatch(action.clone()); 213 | assert_eq!(1, store.get_state().len()); 214 | // cancel the first subscription so we're not caught in an infinite subscriber loop 215 | sub.cancel(); 216 | 217 | let _ = store.dispatch(action.clone()); 218 | assert_eq!(5, store.get_state().len()); 219 | } 220 | 221 | #[test] 222 | fn subscribe_and_cancel_during_subscription_callback() { 223 | let store : Store = Store::new(vec![]); 224 | 225 | // on our first action, sub another subscriber that adds more actions; then cancel 226 | // the first subscription and fire a new event. This should trigger the inner 227 | // subscription to fire, creating events in a loop until we hit 5 228 | store.subscribe(Box::new(move |store, subscription| { 229 | store.subscribe(Box::new(|store, _| { 230 | if store.get_state().len() < 5 { 231 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 232 | let _ = store.dispatch(action); 233 | } 234 | })); 235 | 236 | subscription.cancel(); 237 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 238 | let _ = store.dispatch(action.clone()); 239 | })); 240 | 241 | let action = TodoAction::NewTodo {name: String::from("Grocery Shopping")}; 242 | let _ = store.dispatch(action.clone()); 243 | assert_eq!(5, store.get_state().len()); 244 | } 245 | --------------------------------------------------------------------------------