intrepid-core 0.1.6

Manage complex async business logic with ease
Documentation
use crate::{ActionContext, Frame, FrameFuture, Handler, Router, SystemContext};

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

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

impl<Status, State> Routed<Status, State>
where
    State: Clone + Send + Sync + 'static,
{
    /// Get the action context for this system.
    pub fn action_context(&self) -> ActionContext<State> {
        SystemContext::from(self.router.clone()).into()
    }

    pub fn handle_frame_with_state(&self, frame: Frame, state: State) -> FrameFuture {
        self.router.handle_frame_with_state(frame, state)
    }

    /// Merge another system context into this one.
    fn merge(&mut self, route: impl AsRef<str>, other: SystemContext<State>)
    where
        State: Clone + Send + Sync + 'static,
    {
        self.router
            .scope(route.as_ref(), other.router)
            .expect("Unable to merge given router for {route}");
    }

    /// Add an action to the system.
    pub fn on<ActionHandler, Args>(mut self, route: impl AsRef<str>, action: ActionHandler) -> Self
    where
        ActionHandler: Handler<Args, State> + Clone + Send + Sync + 'static,
        Args: Clone + Send + Sync + 'static,
    {
        if let ActionContext::System(other) = action.context() {
            self.merge(route, other);
        } else {
            self.router
                .insert(route.as_ref(), action)
                .expect("failed to insert route: {route}");
        }

        self
    }

    /// See the routes that have been mounted on the system.
    pub fn routes(&self) -> Vec<String> {
        self.router.routes()
    }
}

impl<State> Routed<Open, State>
where
    State: Clone + Send + Sync + 'static,
{
    /// Create a new routed system with a given state. Routed systems mount actions
    /// with paths and methods, allowing for more complex routing that matches against
    /// parts of a message frame's address.
    pub fn init() -> Self {
        Self {
            router: Router::new(),
            status: Open,
        }
    }

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

impl Routed<Open, ()> {
    /// Create a new routed system without state.
    pub fn without_state(self) -> Routed<Stateless, ()> {
        Routed {
            router: self.router,
            status: Stateless,
        }
    }

    pub fn handle_frame(self, frame: Frame) -> FrameFuture {
        self.handle_frame_with_state(frame, ())
    }
}

impl Routed<Stateless, ()> {
    pub fn handle_frame(self, frame: Frame) -> FrameFuture {
        self.handle_frame_with_state(frame, ())
    }
}

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

    pub fn handle_frame(self, frame: Frame) -> FrameFuture {
        let state = self.state();
        self.handle_frame_with_state(frame, state)
    }
}

impl<Status, State> std::fmt::Debug for Routed<Status, State> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Routed")
            .field("router", &self.router)
            .finish()
    }
}

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

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

    Ok(())
}

#[tokio::test]
async fn targeting_a_root_path() -> Result<(), tower::BoxError> {
    assert_eq!(
        Routed::init()
            .on("/", || async { "hi".to_string() })
            .handle_frame(Frame::message("/", (), ()))
            .await?,
        Frame::from("hi".to_string())
    );

    Ok(())
}

#[tokio::test]
async fn targeting_a_nested_path() -> Result<(), tower::BoxError> {
    use crate::System;

    let health = System::routed().on("/", || async { "health".to_string() });
    let greetings = System::routed().on("/", || async { "hello".to_string() });
    let albums = System::routed().on("/:id", || async { "albums".to_string() });
    let search = System::routed().on("/:fragments*", || async { "search".to_string() });

    let api = System::routed()
        .on("/health", health)
        .on("/greetings", greetings)
        .on("/albums", albums)
        .on("/search", search)
        .on("/", || async { "api root".to_string() });

    let router = Routed::init().on("/api", api);

    assert_eq!(
        router
            // TODO: Cloning? Why isn't this by reference?
            .clone()
            .handle_frame(Frame::message("/api", (), ()))
            .await?,
        Frame::from("api root".to_string())
    );

    assert_eq!(
        router
            .clone()
            .handle_frame(Frame::message("/api/greetings", (), ()))
            .await?,
        Frame::from("hello".to_string())
    );

    assert_eq!(
        router
            .clone()
            .handle_frame(Frame::message("/api/albums/1", (), ()))
            .await?,
        Frame::from("albums".to_string())
    );

    assert_eq!(
        router
            .clone()
            .handle_frame(Frame::message(
                "/api/search/a/bunch/of/search/paths",
                (),
                ()
            ))
            .await?,
        Frame::from("search".to_string())
    );

    Ok(())
}