gstore 0.0.2

Global state management for GTK applications
Documentation
//! # gstore
//!
//! Global state management for GTK Apps.
//!
//! gstore provides state management for GTK applications.

use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use std::sync::mpsc::{channel, Receiver, RecvError, SendError, Sender, TryRecvError};
use std::sync::{Mutex, PoisonError};
use std::thread::JoinHandle;

/// The store is a thing that manages mutations for a given state based on actions.
///
/// A State can be any kind of data structure an application is based on. For a 'counter' app it
/// might be a struct with a single u32 field.
/// Actions represent the possible features of the app affecting the state. In the example above
/// this might be an enum with the values `Increment` and `Decrement`.
///
/// A Store works with two threads: The main UI thread and a background thread. TODO explain.
///
/// # Types
/// A: The Action of the Store
/// S: The State of the Store
///
/// # Examples
/// ```rust
///
/// #[macro_use]
/// extern crate lazy_static;
///
///
/// use std::rc::Rc;
/// use std::sync::Mutex;
///
/// use gstore::{ combine_reducers, Store };
///
///
/// #[derive(Debug, Clone)]
/// struct State {
///    count: u32,
/// }
///
/// #[derive(Debug, Clone, Eq, PartialEq)]
/// enum Action {
///     Increment,
///     Decrement,
///     Shutdown,
/// }
///
/// lazy_static! {
///     static ref STATE: Mutex<State> = Mutex::new(State { count: 0 });
/// }
///
/// fn main() {
///     let mutex = Mutex::new(State { count: 0 });
///     let store: Rc<Store<Action, State>> = Rc::new(Store::new(&STATE));
///     let join = combine_reducers(store.clone(), &STATE, |action, state| match action {
///         Action::Increment => Some(State {
///             count: state.count + 1,
///         }),
///         Action::Decrement => Some(State {
///          count: state.count - 1,
///         }),
///         Action::Shutdown => None,
///     });
///     store.send(Action::Increment);
///     store.send(Action::Increment);
///     store.send(Action::Increment);
///     store.send(Action::Decrement);
///     store.send(Action::Shutdown);
///     join.join().unwrap().expect("Error during Store handling.");
///     assert_eq!(STATE.lock().unwrap().count, 2);
/// }
/// ```
///
pub struct Store<A, S>
where
    A: Send + Clone + Eq + PartialEq + 'static,
    S: Send + Clone + 'static,
{
    sender: Sender<A>,
    receiver: Receiver<A>,
    state: &'static Mutex<S>,
    connection: std::cell::Cell<Option<Sender<A>>>,
    callbacks: Mutex<Vec<Box<dyn Fn(A, &S) + 'static>>>,
}

impl<A, S> Store<A, S>
where
    A: Send + Clone + Eq + PartialEq + 'static,
    S: Send + Clone + 'static,
{
    /// Creates a new state.
    ///
    /// Creates a new state with the reference to the static State mutex.
    pub fn new(state: &'static Mutex<S>) -> Self {
        let (sender, receiver) = channel();
        Store {
            sender,
            receiver,
            state,
            connection: std::cell::Cell::new(None),
            callbacks: Mutex::new(Vec::new()),
        }
    }

    /// Sends the given `Action` to the store.
    ///
    /// This will trigger the reducers as well as notify all receivers (after the reducers) to
    /// handle the state change.
    pub fn send(&self, action: A) {
        if let Some(sender) = self.connection.take() {
            sender.send(action).expect("Could not send action.");
            self.connection.set(Some(sender));
        }
    }

    /// Registers the callback in the store.
    ///
    /// # Arguments
    /// callback: A closure which receives a sent `Action` and the `State` **after** the reducers
    /// were applied.
    pub fn receive(&self, callback: impl Fn(A, &S) + 'static) {
        match self.callbacks.lock() {
            Ok(mut cb) => {
                cb.deref_mut().push(Box::new(callback));
            }
            Err(_) => {}
        }
    }

    /// Send the given action from the ui thread. May only be used in the connected UI thread handler.
    pub fn ui_send(&self, action: A) {
        match self.callbacks.lock() {
            Ok(callbacks_lock) => match self.state.lock() {
                Ok(state) => {
                    for callback in callbacks_lock.deref() {
                        callback(action.clone(), state.deref());
                    }
                }
                Err(_) => {}
            },
            Err(_) => {}
        }
    }

    // Try to receive an Action from the Store. May only be used in the connected UI thread handler.
    pub fn ui_try_receive(&self) -> Result<A, TryRecvError> {
        self.receiver.try_recv()
    }

    fn connect(&self) -> (Sender<A>, Receiver<A>) {
        let (s, r) = channel();
        self.connection.set(Some(s.clone()));
        (self.sender.clone(), r)
    }
}

/// Represents all possible runtime errors of the Store.
///
/// Those are either: RecvErrors or SendErrors of the internally used channel or PoisonErrors when
/// accessing the State mutex fails.
#[derive(Debug)]
pub struct StoreError {
    message: String,
    cause: String,
}

impl StoreError {
    pub fn message(&self) -> &str {
        &self.message
    }
    pub fn cause(&self) -> &str {
        &self.cause
    }
}

impl From<RecvError> for StoreError {
    fn from(re: RecvError) -> Self {
        StoreError {
            message: "Error in gstores channel communication.".to_string(),
            cause: format!("RecvError: {}", re),
        }
    }
}

impl<T> From<SendError<T>> for StoreError {
    fn from(re: SendError<T>) -> Self {
        StoreError {
            message: "Error in gstores channel communication.".to_string(),
            cause: format!("SendError: {}", re),
        }
    }
}

impl<T> From<PoisonError<T>> for StoreError {
    fn from(e: PoisonError<T>) -> Self {
        StoreError {
            message: "".to_string(),
            cause: format!("{:?}", e),
        }
    }
}

/// Applies the given reducer to the store.
pub fn combine_reducers<A, S>(
    store: Rc<Store<A, S>>,
    state: &'static Mutex<S>,
    reducer: impl Fn(A, S) -> Option<S> + Send + 'static,
) -> JoinHandle<Result<(), StoreError>>
where
    A: Send + Clone + Eq + PartialEq + 'static,
    S: Send + Clone + 'static,
{
    let (sender, receiver) = store.connect();
    return std::thread::spawn(move || {
        loop {
            let action = receiver.recv()?;
            let mut state = state.lock()?;
            let next_state = reducer(action.clone(), state.clone());
            if next_state.is_none() {
                break;
            }
            *state = next_state.unwrap();
            sender.send(action.clone())?;
        }
        return Ok(());
    });
}

#[cfg(test)]
#[macro_use]
extern crate lazy_static;

#[cfg(test)]
mod tests {
    use std::rc::Rc;
    use std::sync::Mutex;

    use crate::{combine_reducers, Store};

    #[derive(Debug, Clone)]
    struct State {
        count: u32,
    }

    #[derive(Debug, Clone, Eq, PartialEq)]
    enum Action {
        Increment,
        Decrement,
        Shutdown,
    }

    lazy_static! {
        static ref STATE: Mutex<State> = Mutex::new(State { count: 0 });
    }

    #[test]
    fn test_counter() {
        let store: Rc<Store<Action, State>> = Rc::new(Store::new(&STATE));
        let join = combine_reducers(store.clone(), &STATE, |action, state| match action {
            Action::Increment => Some(State {
                count: state.count + 1,
            }),
            Action::Decrement => Some(State {
                count: state.count - 1,
            }),
            Action::Shutdown => None,
        });
        store.send(Action::Increment);
        store.send(Action::Increment);
        store.send(Action::Increment);
        store.send(Action::Decrement);

        store.send(Action::Shutdown);
        join.join().unwrap().expect("Error during Store handling.");

        assert_eq!(STATE.lock().unwrap().count, 2);
    }
}