intrepid-core 0.3.0

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

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

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

/// A system that invokes the first action that matches a frame. This system
/// tolerates errors in actions, using them as a filter.
///
#[derive(Clone)]
pub struct FirstMatch<Status, State> {
    actions: Actions<State>,
    status: Status,
}

impl<Status, State> FirstMatch<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 { Ok(handle_first_match(actions, state, frame).await) },
        )
    }
}

impl<State> FirstMatch<Open, State> {
    /// Create a new first match system with a given state. First match systems do not route
    /// message frames to matching patterns, but instead invoke their actions in the order they
    /// were added, until they find one that resolves into an Ok result.
    ///
    /// That means that the action handlers can leverage extractors to filter out frames that
    /// they cannot handle, allowing the system to continue to the next action.
    ///
    pub fn init() -> Self {
        FirstMatch {
            status: Open,
            actions: Actions::new(),
        }
    }

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

impl FirstMatch<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) -> FirstMatch<Stateless, ()> {
        FirstMatch {
            actions: self.actions,
            status: Stateless,
        }
    }
}

impl FirstMatch<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> FirstMatch<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().clone();

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

async fn handle_first_match<State>(
    actions: Vec<BoxedAction<State>>,
    state: State,
    frame: Frame,
) -> Frame
where
    State: Clone + Send + 'static,
{
    for action in actions.into_iter() {
        if let Ok(response) = action
            .into_actionable(state.clone())
            .call(frame.clone())
            .await
        {
            return response;
        }
    }

    Frame::default()
}

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

    assert_eq!(
        FirstMatch::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 = FirstMatch::init()
        .with_state(ArbitraryState)
        .handle_frame(Frame::default())
        .await;

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

    Ok(())
}

#[tokio::test]
async fn getting_a_match() -> Result<(), tower::BoxError> {
    use crate::{Context, Extractor, State};

    async fn echo(frame: Frame, _: State<ArbitraryState>) -> Frame {
        frame
    }

    async fn double_echo(frame: Frame, _: HelloWorld) -> Vec<Frame> {
        vec![frame.clone(), frame]
    }

    #[derive(Clone)]
    struct HelloWorld;

    impl Extractor<ArbitraryState> for HelloWorld {
        type Error = String;

        fn extract(frame: Frame, _: &Context<ArbitraryState>) -> Result<Self, Self::Error> {
            if frame.as_ref() == b"Hello, world!" {
                Ok(Self)
            } else {
                Err("No match".to_string())
            }
        }
    }

    #[derive(Clone)]
    struct ArbitraryState;

    let system = FirstMatch::init().on_frame(double_echo).on_frame(echo);

    assert_eq!(
        system
            .with_state(ArbitraryState)
            .handle_frame(Frame::from(String::from("Hello, world!")))
            .await?,
        Frame::from(String::from("Hello, world!Hello, world!"))
    );

    assert_eq!(
        system
            .with_state(ArbitraryState)
            .handle_frame(Frame::from(String::from("echo!")))
            .await?,
        Frame::from(String::from("echo!"))
    );

    Ok(())
}