intrepid_core/extract/
path.rs

1use serde::de::DeserializeOwned;
2
3use crate::{ActionContext, Context, Extractor, Frame, MessageFrame, PathError};
4
5/// A path extractor that captures a given type from a frame. This is used to
6/// extract path captures from a message frame's URI, turning them into local
7/// types. It obeys a few rules:
8///
9/// - It must be used with a list-friendly type, such as `Path<(String,)>`. This
10///   is because, under the hood, it grabs a list of all path captures and then
11///   deserializes them into the given types. That means it's always expecting
12///   some kind of list type. If all the types are uniform, you can use a Vec or
13///   an array.
14/// - If it's a tuple, the number of types must match the number of captures in
15///   the path. If it's a list, the number of types must be equal to or less than
16///   the number of captures in the path.
17/// - The given type must be deserializable from the captures. This means that
18///   the type must implement `DeserializeOwned` or 'Deserialize'.
19///
20/// If any of these rules are broken, the extractor will fail to extract with a
21/// runtime rejection.
22///
23/// # Example
24///
25/// ```rust
26/// use intrepid::{Path, Frame, System, Action};
27///
28/// #[tokio::main]
29/// async fn main() -> intrepid::Result<()> {
30///    let system = System::routed().on("/uri/:id", |Path((id,)): Path<(uuid::Uuid,)>| async { id });
31///
32///    let id = uuid::Uuid::new_v4();
33///    let response = system.call(Frame::message(format!("/uri/{id}")).await?;
34///
35///    assert_eq!(response, id);
36/// }
37/// ```
38///
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct Path<T>(pub T)
41where
42    T: DeserializeOwned;
43
44impl<State, T> Extractor<State> for Path<T>
45where
46    T: DeserializeOwned,
47    State: std::fmt::Debug + Clone + Send + Sync + 'static,
48{
49    type Error = PathError;
50
51    fn extract(frame: Frame, context: &Context<State>) -> Result<Self, Self::Error> {
52        match (frame, &context.action) {
53            (Frame::Message(MessageFrame { uri, .. }), ActionContext::System(system_context)) => {
54                let capture = system_context.router.capture::<T>(uri)?;
55
56                Ok(Self(capture))
57            }
58            (_, ActionContext::System(_)) => Err(PathError::FrameIsNotAMessage),
59            _ => Err(PathError::IncorrectActionContext),
60        }
61    }
62}
63
64#[tokio::test]
65async fn attempts_to_extract_given_types() -> Result<(), tower::BoxError> {
66    use crate::SystemContext;
67
68    let id = uuid::Uuid::new_v4();
69    let frame = Frame::message(format!("/uri/{id}"), (), ());
70    let mut router = crate::Router::<()>::new();
71
72    router.insert("/uri/:id", || async {})?;
73
74    let context = Context::from_action(SystemContext::from(router).into());
75
76    let Path((extracted,)): Path<(uuid::Uuid,)> = Path::extract(frame, &context)?;
77
78    assert_eq!(extracted, id);
79
80    Ok(())
81}
82
83#[tokio::test]
84async fn non_frame_messages_error() -> Result<(), tower::BoxError> {
85    use crate::{Router, SystemContext};
86
87    let frame = Frame::default();
88    let context = Context::from_action(SystemContext::<()>::from(Router::default()).into());
89
90    let attempt = Path::<()>::extract(frame, &context);
91
92    assert!(
93        matches!(attempt.unwrap_err(), PathError::FrameIsNotAMessage),
94        "expected a frame is not a message error"
95    );
96
97    Ok(())
98}
99
100#[tokio::test]
101async fn non_system_contexts_error() -> Result<(), tower::BoxError> {
102    let frame = Frame::default();
103    let context = Context::<()>::default();
104    let attempt = Path::<()>::extract(frame, &context);
105
106    assert!(
107        matches!(attempt.unwrap_err(), PathError::IncorrectActionContext),
108        "expected an incorrect action context error"
109    );
110
111    Ok(())
112}