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(())
}