intrepid_core/extract/
path.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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(())
}