intrepid-core 0.1.6

Manage complex async business logic with ease
Documentation
use tower::Service;

use crate::{Action, ActionContext, Actions, Frame, FrameFuture, Handler};

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

#[derive(Clone)]
/// A system that routes frames to matching patterns.
pub struct Dispatch<Status, State> {
    actions: Actions<State>,
    status: Status,
}

impl<Status, State> Dispatch<Status, State>
where
    State: Clone + Send + Sync + 'static,
{
    /// Add an action to the system.
    pub fn on<ActionHandler, Args>(
        mut self,
        pattern: impl AsRef<str>,
        action: ActionHandler,
    ) -> Self
    where
        ActionHandler: Handler<Args, State> + Clone + Send + Sync + 'static,
        Args: Clone + Send + Sync + 'static,
    {
        // TODO: Action should be able to store layers for eventual invocation.
        let action = Dispatchable::new(pattern.as_ref(), Action::new(action));

        self.actions.push(Box::new(action));
        self
    }

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

    /// Handle frame with state using this system.
    pub fn handle_frame_with_state(&self, frame: Frame, state: State) -> FrameFuture {
        let mut responses = vec![];

        for action in self.actions.clone().into_iter() {
            responses.push(action.into_actionable(state.clone()).call(frame.clone()));
        }

        FrameFuture::from_frame_futures(responses)
    }
}

impl<State> Dispatch<Open, State> {
    pub fn init() -> Self {
        Self {
            actions: Vec::new(),
            status: Open,
        }
    }

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

impl Dispatch<Open, ()> {
    pub fn without_state(self) -> Dispatch<Stateless, ()> {
        Dispatch {
            actions: self.actions,
            status: Stateless,
        }
    }

    /// Handle a frame with this system.
    pub fn handle_frame(self, frame: Frame) -> FrameFuture {
        self.handle_frame_with_state(frame, ())
    }
}

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

impl<State> Dispatch<Stateful<State>, State>
where
    State: Clone + Send + Sync + 'static,
{
    /// Handle a frame with this system.
    pub fn handle_frame(self, frame: Frame) -> FrameFuture {
        let state = self.state();

        self.handle_frame_with_state(frame, state)
    }

    /// Get the state for this system.
    pub fn state(&self) -> State {
        self.status.0.clone()
    }
}

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

    assert_eq!(
        Dispatch::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;

    assert_eq!(
        Dispatch::init()
            .with_state(ArbitraryState)
            .handle_frame(Frame::default())
            .await?,
        Frame::default()
    );

    Ok(())
}

#[tokio::test]
async fn calls_full_matches_statelessly() -> Result<(), tower::BoxError> {
    let string_id = |given_string: String| async { given_string };
    let system = Dispatch::init().on("match", string_id);

    let message = Frame::message("match", "test".to_owned(), ());
    let result = system.without_state().handle_frame(message).await;

    assert_eq!(Frame::from(result), "test".to_string().into());

    Ok(())
}

#[tokio::test]
async fn calls_full_matches_with_state() -> Result<(), tower::BoxError> {
    #[derive(Clone)]
    struct ArbitraryState;

    let string_id = |given_string: String, _: crate::State<ArbitraryState>| async { given_string };
    let system = Dispatch::init().on("match", string_id);

    let message = Frame::message("match", "test".to_owned(), ());
    let result = system
        .with_state(ArbitraryState)
        .handle_frame(message)
        .await;

    assert_eq!(Frame::from(result), "test".to_string().into());

    Ok(())
}

#[tokio::test]
async fn ignores_stateless_misses() -> Result<(), tower::BoxError> {
    let string_id = |given_string: String| async { given_string };
    let system = Dispatch::init().on("match", string_id);

    let message = Frame::message("miss", "test".to_owned(), ());
    let result = system.without_state().handle_frame(message).await;

    assert_eq!(Frame::from(result), ().into());

    Ok(())
}

#[tokio::test]
async fn ignores_stateful_misses() -> Result<(), tower::BoxError> {
    #[derive(Clone)]
    struct ArbitraryState;

    let string_id = |given_string: String, _: crate::State<ArbitraryState>| async { given_string };
    let system = Dispatch::init().on("match", string_id);

    let message = Frame::message("miss", "test".to_owned(), ());
    let result = system
        .with_state(ArbitraryState)
        .handle_frame(message)
        .await;

    assert_eq!(Frame::from(result), ().into());

    Ok(())
}