intrepid-core 0.1.6

Manage complex async business logic with ease
Documentation
use serde::de::DeserializeOwned;

use crate::{ActionContext, Context, Extractor, Frame, MessageFrame, PathError};

/// A path extractor that captures a given type from a frame. This is used to
/// extract path captures from a message frame's URI, turning them into local
/// types. It obeys a few rules:
///
/// - It must be used with a list-friendly type, such as `Path<(String,)>`. This
///   is because, under the hood, it grabs a list of all path captures and then
///   deserializes them into the given types. That means it's always expecting
///   some kind of list type. If all the types are uniform, you can use a Vec or
///   an array.
/// - If it's a tuple, the number of types must match the number of captures in
///   the path. If it's a list, the number of types must be equal to or less than
///   the number of captures in the path.
/// - The given type must be deserializable from the captures. This means that
///   the type must implement `DeserializeOwned` or 'Deserialize'.
///
/// If any of these rules are broken, the extractor will fail to extract with a
/// runtime rejection.
///
/// # Example
///
/// ```rust
/// use intrepid::{Path, Frame, System, Action};
///
/// #[tokio::main]
/// async fn main() -> intrepid::Result<()> {
///    let system = System::routed().on("/uri/:id", |Path((id,)): Path<(uuid::Uuid,)>| async { id });
///
///    let id = uuid::Uuid::new_v4();
///    let response = system.call(Frame::message(format!("/uri/{id}")).await?;
///
///    assert_eq!(response, id);
/// }
/// ```
///
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Path<T>(pub T)
where
    T: DeserializeOwned;

impl<State, T> Extractor<State> for Path<T>
where
    T: DeserializeOwned,
    State: std::fmt::Debug + Clone + Send + Sync + 'static,
{
    type Error = PathError;

    fn extract(frame: Frame, context: &Context<State>) -> Result<Self, Self::Error> {
        match (frame, &context.action) {
            (Frame::Message(MessageFrame { uri, .. }), ActionContext::System(system_context)) => {
                let capture = system_context.router.capture::<T>(uri)?;

                Ok(Self(capture))
            }
            (_, ActionContext::System(_)) => Err(PathError::FrameIsNotAMessage),
            _ => Err(PathError::IncorrectActionContext),
        }
    }
}

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

    let id = uuid::Uuid::new_v4();
    let frame = Frame::message(format!("/uri/{id}"), (), ());
    let mut router = crate::Router::<()>::new();

    router.insert("/uri/:id", || async {})?;

    let context = Context::from_action(SystemContext::from(router).into());

    let Path((extracted,)): Path<(uuid::Uuid,)> = Path::extract(frame, &context)?;

    assert_eq!(extracted, id);

    Ok(())
}

#[tokio::test]
async fn non_frame_messages_error() -> Result<(), tower::BoxError> {
    use crate::{Router, SystemContext};

    let frame = Frame::default();
    let context = Context::from_action(SystemContext::<()>::from(Router::default()).into());

    let attempt = Path::<()>::extract(frame, &context);

    assert!(
        matches!(attempt.unwrap_err(), PathError::FrameIsNotAMessage),
        "expected a frame is not a message error"
    );

    Ok(())
}

#[tokio::test]
async fn non_system_contexts_error() -> Result<(), tower::BoxError> {
    let frame = Frame::default();
    let context = Context::<()>::default();
    let attempt = Path::<()>::extract(frame, &context);

    assert!(
        matches!(attempt.unwrap_err(), PathError::IncorrectActionContext),
        "expected an incorrect action context error"
    );

    Ok(())
}