gstore 0.10.4

Global and local state management in redux style for applications written in Rust
Documentation
// SPDX-License-Identifier: GPL-3.0-or-later

use std::{
    borrow::Borrow,
    cell::RefCell,
    collections::{HashMap, LinkedList},
    rc::Rc,
    sync::Arc,
};

use async_trait::async_trait;
use uuid::Uuid;

/// An Actionable is something that can be handled by gstore as an action.
pub trait Actionable: Sync + Send {
    fn name(&self) -> &'static str;
    fn list() -> Vec<Self>
    where
        Self: Sized;
    fn try_from_name(name: &str) -> Option<Self>
    where
        Self: Sized;
    fn keybinding(&self) -> Option<Keybinding> {
        None
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum Keybinding {
    Shift(&'static str),
    Super(&'static str),
    Alt(&'static str),
    Ctrl(&'static str),
    Just(&'static str),
}

type Reducer<A, S> = Box<dyn Fn(&A, &mut S)>;

type ArcMiddleware<A, S> = Arc<Box<dyn Middleware<A, S>>>;

/// The global store.
///
/// Calls reducers and middlewares according to action provided via Store::dispatch.
///
/// To get a notification for a state change you can register Selectors via `store().select(SELECTOR, || { ... })`
///
/// To append middlewares call `store().append(Box::new(MyMiddleware))`.
pub struct Store<A, S>
where
    A: Actionable + Clone + std::fmt::Debug + Send + Sync + 'static,
    S: Clone + std::fmt::Debug + Default + Send + Sync + 'static,
{
    sender: &'static Sender<A>,
    state: RefCell<S>,
    reducer: RefCell<Option<Reducer<A, S>>>,
    middlewares: RefCell<LinkedList<ArcMiddleware<A, S>>>,
    selectors: RefCell<HashMap<Uuid, Selector<S>>>,
}

impl<
        A: Actionable + Clone + std::fmt::Debug + Send + Sync + 'static,
        S: Clone + std::fmt::Debug + Default + Send + Sync + 'static,
    > std::fmt::Debug for Store<A, S>
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Store")
            .field("state", &self.state)
            .field("reducer", &self.reducer.borrow().is_some())
            .field("middlewares", &self.middlewares.borrow().len())
            .field("selectors", &self.selectors.borrow().len())
            .finish()
    }
}

/// A middleware can be appended to a Store instance to run async code in relation to sync Actions.
#[async_trait]
pub trait Middleware<A, S>: Sync + Send {
    async fn pre_reduce(&self, _: Arc<A>, _: Arc<S>);
    async fn post_reduce(&self, _: Arc<A>, _: Arc<S>);
}

type Selection<S> = Rc<dyn Fn(&S, &S) -> bool + 'static>;

#[derive(Clone)]
struct Selector<S: Clone> {
    name: &'static str,
    selector: Selection<S>,
    callback: Rc<dyn Fn(&S) + 'static>,
}

impl<A, S> Store<A, S>
where
    A: Actionable + Clone + std::fmt::Debug + Send + Sync + 'static,
    S: Clone + std::fmt::Debug + Default + Send + Sync + 'static,
{
    /// Creates a new store instance.
    pub fn new(sender: &'static Sender<A>) -> Self {
        Self {
            sender,
            reducer: Default::default(),
            state: Default::default(),
            middlewares: Default::default(),
            selectors: Default::default(),
        }
    }

    /// Initializes the store.
    pub fn init(
        self: &Rc<Self>,
        reducer: impl Fn(&A, &mut S) + 'static,
        send_action: impl Fn(A) + Send + Sync + 'static,
    ) -> Box<dyn Fn(A)> {
        *self.sender.try_write().unwrap() = Some(Box::new(send_action));
        *self.reducer.borrow_mut() = Some(Box::new(reducer));
        let s = self.clone();
        Box::new(move |a| s.reduce(a))
    }

    /// Reduce the given action in this store.
    pub fn reduce(self: &Rc<Self>, action: A) {
        log::trace!("[gstore] Reduce {:?}", action);
        let old_state = Arc::new(self.state.borrow().clone());

        let middlewares: LinkedList<Arc<Box<dyn Middleware<A, S>>>> =
            self.middlewares.borrow().iter().cloned().collect();

        let mi_a = Arc::new(action.clone());
        let mi_s = old_state.clone();

        async_std::task::spawn(async move {
            for mi in middlewares {
                mi.pre_reduce(mi_a.clone(), mi_s.clone()).await;
            }
        });

        if let Some(r) = &*self.reducer.borrow() {
            r(&action, &mut *self.state.borrow_mut());
        }

        self.call_selectors(old_state.borrow());

        let middlewares: LinkedList<Arc<Box<dyn Middleware<A, S>>>> =
            self.middlewares.borrow().iter().cloned().collect();

        let mi_a = Arc::new(action);
        let mi_s = Arc::new(self.state.borrow().clone());
        async_std::task::spawn(async move {
            for mi in middlewares {
                mi.post_reduce(mi_a.clone(), mi_s.clone()).await;
            }
        });
    }

    /// Register a middleware to a store.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # #[derive(Debug, Clone, Eq, PartialEq)]
    /// # enum Action {}
    ///
    /// # #[derive(Debug, Clone, Default, Eq, PartialEq)]
    /// # struct State { some_field: u32 }
    ///
    /// # impl gstore::Actionable for Action {
    /// # fn name(&self) -> &'static str { "" }
    /// # fn list() -> Vec<Self> { vec![] }
    /// # fn try_from_name(name: &str) -> Option<Action> { None }
    /// # }
    ///
    /// # gstore::store!(Action, State);
    ///
    /// const MY_SELECTOR: &'static Selector = &|old_state, new_state| {
    ///     old_state.some_field != new_state.some_field
    /// };
    ///
    /// struct MyMiddleware;
    /// #[async_trait::async_trait]
    /// impl gstore::Middleware<Action,State> for MyMiddleware {
    ///     async fn pre_reduce(&self, _: std::sync::Arc<Action>, _: std::sync::Arc<State>) {
    ///         // do smth async
    ///     }
    ///     async fn post_reduce(&self, _: std::sync::Arc<Action>, _: std::sync::Arc<State>) {
    ///         // do smth async
    ///     }
    /// }
    ///
    /// store().append(Box::new(MyMiddleware));
    /// ```
    pub fn append(self: &Rc<Self>, middleware: Box<dyn Middleware<A, S>>) {
        self.middlewares
            .borrow_mut()
            .push_back(Arc::new(middleware));
    }

    fn call_selectors(&self, old_state: &S) {
        let borrow = self.selectors.borrow().clone();
        for selector in borrow.values() {
            log::trace!("call selector for {}", selector.name);
            let sel = &selector.selector;
            let state = &*self.state.borrow();
            let updated = sel(old_state, state);
            if updated {
                let cb = &selector.callback;
                cb(state);
            }
        }
    }

    /// Register a handler for a defined state change.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// # #[derive(Debug, Clone, Eq, PartialEq)]
    /// # enum Action {}
    /// # #[derive(Debug, Clone, Default, Eq, PartialEq)]
    /// # struct State { some_field: u32 }
    /// # impl gstore::Actionable for Action {
    /// # fn name(&self) -> &'static str { "" }
    /// # fn list() -> Vec<Self> { vec![] }
    /// # fn try_from_name(name: &str) -> Option<Action> { None }
    /// # }
    /// # gstore::store!(Action, State);
    /// const MY_SELECTOR: &'static Selector = &|old_state, new_state| {
    ///     old_state.some_field != new_state.some_field
    /// };
    ///
    /// let cleanup = store().select(MY_SELECTOR, move |state| {
    ///     // do smth with the state
    /// });
    /// // TODO: handle cleanup
    /// ```
    ///
    /// # Regarding Memory
    ///
    /// calling select will add a callback to the store. You might want to
    /// remove this callback from the store when a component is dropped.
    /// You can to so by calling the returned function.
    ///
    /// In grx it would look like this:
    ///
    /// ```ignore
    /// fn my_component() {
    ///     let grx = grx! {
    ///         ...    
    ///     };
    ///     let component = MyComponent::new(props, grx)
    ///     component.later_drop(store().select(MY_SELECTOR, |state| {
    ///     //        ^
    ///     //        '--------------- Use .later_drop to register this callback to be
    ///     //                         dropped from the store with the components drop.
    ///         ...
    ///     }));
    ///     component
    /// }
    ///
    /// This function returns a Result to remind you on dealing with the returned cleanup callback. It will never error.
    /// ```
    #[allow(clippy::result_unit_err)]
    pub fn select(
        self: &Rc<Self>,
        selector: impl Fn(&S, &S) -> bool + 'static,
        handler: impl Fn(&S) + 'static,
    ) -> Result<Rc<dyn Fn()>, ()> {
        self.select_dbg("", selector, handler)
    }
    #[allow(clippy::result_unit_err)]
    pub fn select_dbg(
        self: &Rc<Self>,
        name: &'static str,
        selector: impl Fn(&S, &S) -> bool + 'static,
        handler: impl Fn(&S) + 'static,
    ) -> Result<Rc<dyn Fn()>, ()> {
        // call selector for initial state:
        handler(&*self.state.borrow());

        let id = Uuid::new_v4();
        self.selectors.borrow_mut().insert(
            id,
            Selector {
                name,
                selector: Rc::new(selector),
                callback: Rc::new(handler),
            },
        );
        let s = self.clone();
        Ok(Rc::new(move || {
            s.selectors.borrow_mut().remove(&id);
        }))
    }

    /// Dispartch an action to the store.
    pub fn dispatch(self: &Rc<Self>, action: impl Into<A>) {
        async_std::task::block_on(async {
            if let Some(sender) = &*self.sender.read().await {
                sender(action.into());
            }
        })
    }
}

pub type Sender<A> = async_std::sync::RwLock<Option<Box<dyn Fn(A) + Sync + Send + 'static>>>;

/// Generate store definitions.
///
/// # Example
///
/// ```rust,no_run
/// #[derive(Debug, Clone, Eq, PartialEq)]
/// enum Action {}
///
/// #[derive(Debug, Clone, Default, Eq, PartialEq)]
/// struct State { some_field: u32 }
///
/// impl gstore::Actionable for Action {
/// fn name(&self) -> &'static str { "" }
/// fn list() -> Vec<Self> { vec![] }
/// fn try_from_name(name: &str) -> Option<Action> { None }
/// }
///
/// gstore::store!(Action, State);
///
/// // do smth with `store()`
/// ```
#[macro_export]
macro_rules! store {
    (
        $action:ty, $state:ty
    ) => {
        impl Into<&str> for $action {
            fn into(self) -> &'static str {
                $crate::Actionable::name(&self)
            }
        }
        impl Into<&str> for &$action {
            fn into(self) -> &'static str {
                $crate::Actionable::name(self)
            }
        }

        type Selector = dyn Fn(&$state, &$state) -> bool;

        static SEND_EVENT: $crate::once_cell::sync::Lazy<$crate::Sender<Action>> =
            $crate::once_cell::sync::Lazy::new(|| Default::default());

        type Store = $crate::Store<$action, $state>;

        thread_local! {
            static STORE: std::rc::Rc<Store> = std::rc::Rc::new(Store::new(&SEND_EVENT));
        }

        pub(crate) fn store() -> std::rc::Rc<Store> {
            STORE.with(|s| s.clone())
        }
    };
}