This commit is contained in:
David Pedersen
2022-02-13 22:08:49 +01:00
parent 1b7326e9de
commit 46f85fd9ba
4 changed files with 126 additions and 20 deletions
+48 -9
View File
@@ -7,7 +7,6 @@ mod resource;
#[cfg(feature = "typed-routing")]
mod typed;
/// TODO(david): docs
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
pub use axum_macros::TypedPath;
@@ -44,7 +43,12 @@ pub trait RouterExt<B>: sealed::Sealed {
where
T: HasRoutes<B>;
/// TODO(david): docs
/// Add a typed `GET` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
fn typed_get<H, T, P>(self, handler: H) -> Self
@@ -53,7 +57,12 @@ pub trait RouterExt<B>: sealed::Sealed {
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
/// Add a typed `DELETE` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
fn typed_delete<H, T, P>(self, handler: H) -> Self
@@ -62,7 +71,12 @@ pub trait RouterExt<B>: sealed::Sealed {
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
/// Add a typed `HEAD` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
fn typed_head<H, T, P>(self, handler: H) -> Self
@@ -71,7 +85,12 @@ pub trait RouterExt<B>: sealed::Sealed {
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
/// Add a typed `OPTIONS` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
fn typed_options<H, T, P>(self, handler: H) -> Self
@@ -80,7 +99,12 @@ pub trait RouterExt<B>: sealed::Sealed {
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
/// Add a typed `PATCH` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
fn typed_patch<H, T, P>(self, handler: H) -> Self
@@ -89,7 +113,12 @@ pub trait RouterExt<B>: sealed::Sealed {
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
/// Add a typed `POST` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
fn typed_post<H, T, P>(self, handler: H) -> Self
@@ -98,7 +127,12 @@ pub trait RouterExt<B>: sealed::Sealed {
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
/// Add a typed `PUT` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
fn typed_put<H, T, P>(self, handler: H) -> Self
@@ -107,7 +141,12 @@ pub trait RouterExt<B>: sealed::Sealed {
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
/// Add a typed `TRACE` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
#[cfg_attr(docsrs, doc(cfg(feature = "typed-routing")))]
fn typed_trace<H, T, P>(self, handler: H) -> Self
+74 -10
View File
@@ -1,10 +1,9 @@
//! Type safe routing.
//!
//! See [`TypedPath`] for more details.
use super::sealed::Sealed;
/// TODO(david): more docs
/// A type safe path.
///
/// This is used to statically connect a path to its corresponding handler using
/// [`RouterExt::typed_get`], [`RouterExt::typed_post`], etc.
///
/// # Example
///
@@ -18,14 +17,14 @@ use super::sealed::Sealed;
/// };
///
/// // A type safe route with `/users/:id` as its associated path.
/// #[derive(Deserialize, TypedPath)]
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// struct UsersMember {
/// id: u32,
/// }
///
/// // A regular handler function that takes `UsersMember` as the first argument
/// // and thus creates a typed connection between this handler and the `/users/:id` route.
/// // and thus creates a typed connection between this handler and the `/users/:id` path.
/// //
/// // The `TypedPath` must be the first argument to the function.
/// async fn users_show(
@@ -63,11 +62,68 @@ use super::sealed::Sealed;
/// #
/// # let app: Router<axum::body::Body> = app;
/// ```
///
/// # Using `#[derive(TypedPath)]`
///
/// While `TypedPath` can be implemented manually its _highly_ recommended to derive it:
///
/// ```
/// # use serde::Deserialize;
/// #
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// struct UsersMember {
/// id: u32,
/// }
/// ```
///
/// The macro expands to:
///
/// - A `TypedPath` implementation.
/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other
/// things, create links to known paths and have them verified statically.
/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`],
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
/// also implement [`serde::Deserialize`], unless its a unit struct.
///
/// Additionally the macro will verify the captures in the path matches the fields of the struct.
/// For example this fails to compile since the struct doesn't have a `team_id` field:
///
/// ```compile_fail
/// # use serde::Deserialize;
/// #
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id/teams/:team_id")]
/// struct UsersMember {
/// id: u32,
/// }
/// ```
///
/// Unit and tuple structs are also supported:
///
/// ```
/// # use serde::Deserialize;
/// #
/// #[derive(TypedPath)]
/// #[typed_path("/users")]
/// struct UsersCollection;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/:id")]
/// struct UsersMember(u32);
/// ```
///
/// [`FromRequest`]: axum::extract::FromRequest
/// [`RouterExt::typed_get`]: super::RouterExt::typed_get
/// [`RouterExt::typed_post`]: super::RouterExt::typed_post
/// [`Path`]: axum::extract::Path
/// [`Display`]: std::fmt::Display
pub trait TypedPath: std::fmt::Display {
/// The path with optional captures such as `/users/:id`.
const PATH: &'static str;
}
/// Utility trait used with [`TypedRouter`] to ensure the first element of a tuple type is a
/// Utility trait used with [`RouterExt`] to ensure the first element of a tuple type is a
/// given type.
///
/// If you see it in type errors its most likely because the first argument to your handler doesn't
@@ -76,13 +132,21 @@ pub trait TypedPath: std::fmt::Display {
/// You normally shouldn't have to use this trait directly.
///
/// It is sealed such that it cannot be implemented outside this crate.
///
/// [`RouterExt`]: super::RouterExt
pub trait FirstElementIs<P>: Sealed {}
macro_rules! impl_first_element_is {
( $($ty:ident),* $(,)? ) => {
impl<P, $($ty,)*> FirstElementIs<P> for (P, $($ty,)*) {}
impl<P, $($ty,)*> FirstElementIs<P> for (P, $($ty,)*)
where
P: TypedPath
{}
impl<P, $($ty,)*> Sealed for (P, $($ty,)*) {}
impl<P, $($ty,)*> Sealed for (P, $($ty,)*)
where
P: TypedPath
{}
};
}
+1
View File
@@ -21,6 +21,7 @@ syn = { version = "1.0", features = ["full"] }
[dev-dependencies]
axum = { path = "../axum", version = "0.4", features = ["headers"] }
axum-extra = { path = "../axum-extra", version = "0.1" }
rustversion = "1.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
+3 -1
View File
@@ -386,7 +386,9 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
return expand_attr_with(_attr, input, debug_handler::expand);
}
/// TODO
/// Derive an implementation of [`axum_extra::routing::TypedPath`].
///
/// See that trait for more details.
#[proc_macro_derive(TypedPath, attributes(typed_path))]
pub fn derive_typed_path(input: TokenStream) -> TokenStream {
expand_with(input, typed_path::expand)