intrepid-core 0.3.0

Manage complex async business logic with ease
Documentation
use crate::{Action, ActionContext, Actions, BoxedAction, Frame, FrameFuture, Handler};

use super::{Open, Stateful, Stateless};

/// A system that is still being initialized.
#[derive(Clone)]
pub struct Batch<Status, State> {
    actions: Actions<State>,
    status: Status,
}

impl<Status, State> Batch<Status, State>
where
    State: Clone + Send + Sync + 'static,
{
    /// Add an action to the system.
    pub fn on_frame<ActionHandler, Args>(mut self, action: ActionHandler) -> Self
    where
        ActionHandler: Handler<Args, State> + Clone + Send + Sync + 'static,
        Args: Clone + Send + Sync + 'static,
    {
        self.actions.push(Box::new(Action::new(action)));
        self
    }

    /// Get the (empty) action context for this system.
    pub fn action_context(&self) -> ActionContext<State> {
        ActionContext::Unit
    }

    /// Handle a frame and state with this system.
    pub fn handle_frame_with_state(&self, frame: Frame, state: State) -> FrameFuture {
        let actions = self.actions.clone();

        FrameFuture::from_async_block(async move { handle_direct(actions, state, frame).await })
    }
}

impl<State> Batch<Open, State> {
    /// Create a new direct system with a given state. Direct systems do not route message
    /// frames to matching patterns, but instead invoke all actions.
    pub fn init() -> Self {
        Batch {
            status: Open,
            actions: Actions::new(),
        }
    }

    /// Transition to a stateful system with a given state.
    pub fn with_state(&self, state: State) -> Batch<Stateful<State>, State> {
        Batch {
            actions: self.actions.clone(),
            status: Stateful(state),
        }
    }
}

impl Batch<Open, ()> {
    /// Handle a frame and state with this system.
    pub fn handle_frame(self, frame: Frame) -> FrameFuture {
        self.handle_frame_with_state(frame, ())
    }

    /// Create a new direct system without state.
    pub fn without_state(self) -> Batch<Stateless, ()> {
        Batch {
            actions: self.actions,
            status: Stateless,
        }
    }
}

impl Batch<Stateless, ()> {
    /// Handle a frame and state with this system.
    pub fn handle_frame(self, frame: Frame) -> FrameFuture {
        self.handle_frame_with_state(frame, ())
    }
}

impl<State> Batch<Stateful<State>, State>
where
    State: Clone + Send + 'static,
{
    /// Get the state for this system.
    pub fn state(&self) -> State {
        self.status.0.clone()
    }

    /// Handle a frame with this system.
    pub fn handle_frame(self, frame: Frame) -> FrameFuture {
        let actions = self.actions.clone();
        let state = self.state();

        FrameFuture::from_async_block(async move { handle_direct(actions, state, frame).await })
    }
}

async fn handle_direct<State>(
    actions: Vec<BoxedAction<State>>,
    state: State,
    frame: Frame,
) -> crate::Result<Frame>
where
    State: Clone + Send + 'static,
{
    let mut responses = vec![];

    for action in actions.into_iter() {
        responses.push(
            action
                .into_actionable(state.clone())
                .handle_frame(frame.clone())
                .await?,
        );
    }

    Ok(responses.into())
}

#[tokio::test]
async fn calling_with_a_frame_statelessly() -> Result<(), tower::BoxError> {
    assert_eq!(
        Batch::init()
            .without_state()
            .handle_frame(Frame::default())
            .await?,
        Frame::default()
    );

    assert_eq!(
        Batch::init().handle_frame(Frame::default()).await?,
        Frame::default()
    );

    Ok(())
}

#[tokio::test]
async fn calling_with_a_frame_statefully() -> Result<(), tower::BoxError> {
    #[derive(Clone)]
    struct ArbitraryState;
    let result = Batch::init()
        .with_state(ArbitraryState)
        .handle_frame(Frame::default())
        .await;

    assert_eq!(result?, Frame::default());

    Ok(())
}