mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-27 00:00:24 +02:00
Type safe routing (#756)
* wip
* wip
* make macro implement trait
* checkpoint
* checkpoint
* Simplify things quite a bit
* re-export `axum_macros::TypedPath` from `axum_extra`
* docs
* add missing feature
* fix docs link
* fix features
* fix missing imports
* make serde an optional dep again
* ui tests
* Break things up a bit
* Update span for `FromRequest` impls to point to callsite
* make docs feature labels show up automatically
* Apply suggestions from code review
Co-authored-by: Jonas Platte <[email protected]>
* add note about Display/Serialize being compatible
* Update axum-extra/src/routing/typed.rs
Co-authored-by: Jonas Platte <[email protected]>
* fix missing docs link
* what about typed methods?
* Revert "what about typed methods?"
This reverts commit cc1f989467.
* don't allow wildcards for now
* percent encode params
* Update axum-extra/src/routing/typed.rs
Co-authored-by: Jonas Platte <[email protected]>
* rephrase args
* changelog
Co-authored-by: Jonas Platte <[email protected]>
This commit is contained in:
co-authored by
Jonas Platte
parent
d12494cc9c
commit
7a228a584b
@@ -1,11 +1,20 @@
|
||||
//! Additional types for defining routes.
|
||||
|
||||
use axum::{body::Body, Router};
|
||||
use axum::{body::Body, handler::Handler, Router};
|
||||
|
||||
mod resource;
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
mod typed;
|
||||
|
||||
pub use self::resource::Resource;
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
pub use axum_macros::TypedPath;
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
pub use self::typed::{FirstElementIs, TypedPath};
|
||||
|
||||
/// Extension trait that adds additional methods to [`Router`].
|
||||
pub trait RouterExt<B>: sealed::Sealed {
|
||||
/// Add the routes from `T`'s [`HasRoutes::routes`] to this router.
|
||||
@@ -32,6 +41,110 @@ pub trait RouterExt<B>: sealed::Sealed {
|
||||
fn with<T>(self, routes: T) -> Self
|
||||
where
|
||||
T: HasRoutes<B>;
|
||||
|
||||
/// 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")]
|
||||
fn typed_get<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath;
|
||||
|
||||
/// 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")]
|
||||
fn typed_delete<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath;
|
||||
|
||||
/// 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")]
|
||||
fn typed_head<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath;
|
||||
|
||||
/// 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")]
|
||||
fn typed_options<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath;
|
||||
|
||||
/// 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")]
|
||||
fn typed_patch<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath;
|
||||
|
||||
/// 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")]
|
||||
fn typed_post<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath;
|
||||
|
||||
/// 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")]
|
||||
fn typed_put<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath;
|
||||
|
||||
/// 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")]
|
||||
fn typed_trace<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath;
|
||||
}
|
||||
|
||||
impl<B> RouterExt<B> for Router<B>
|
||||
@@ -44,6 +157,86 @@ where
|
||||
{
|
||||
self.merge(routes.routes())
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
fn typed_get<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath,
|
||||
{
|
||||
self.route(P::PATH, axum::routing::get(handler))
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
fn typed_delete<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath,
|
||||
{
|
||||
self.route(P::PATH, axum::routing::delete(handler))
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
fn typed_head<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath,
|
||||
{
|
||||
self.route(P::PATH, axum::routing::head(handler))
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
fn typed_options<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath,
|
||||
{
|
||||
self.route(P::PATH, axum::routing::options(handler))
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
fn typed_patch<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath,
|
||||
{
|
||||
self.route(P::PATH, axum::routing::patch(handler))
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
fn typed_post<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath,
|
||||
{
|
||||
self.route(P::PATH, axum::routing::post(handler))
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
fn typed_put<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath,
|
||||
{
|
||||
self.route(P::PATH, axum::routing::put(handler))
|
||||
}
|
||||
|
||||
#[cfg(feature = "typed-routing")]
|
||||
fn typed_trace<H, T, P>(self, handler: H) -> Self
|
||||
where
|
||||
H: Handler<T, B>,
|
||||
T: FirstElementIs<P> + 'static,
|
||||
P: TypedPath,
|
||||
{
|
||||
self.route(P::PATH, axum::routing::trace(handler))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for things that can provide routes.
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
use super::sealed::Sealed;
|
||||
|
||||
/// 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
|
||||
///
|
||||
/// ```rust
|
||||
/// use serde::Deserialize;
|
||||
/// use axum::{Router, extract::Json};
|
||||
/// use axum_extra::routing::{
|
||||
/// TypedPath,
|
||||
/// RouterExt, // for `Router::typed_*`
|
||||
/// };
|
||||
///
|
||||
/// // A type safe route with `/users/:id` as its associated path.
|
||||
/// #[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` path.
|
||||
/// //
|
||||
/// // The `TypedPath` must be the first argument to the function.
|
||||
/// async fn users_show(
|
||||
/// UsersMember { id }: UsersMember,
|
||||
/// ) {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new()
|
||||
/// // Add our typed route to the router.
|
||||
/// //
|
||||
/// // The path will be inferred to `/users/:id` since `users_show`'s
|
||||
/// // first argument is `UsersMember` which implements `TypedPath`
|
||||
/// .typed_get(users_show)
|
||||
/// .typed_post(users_create)
|
||||
/// .typed_delete(users_destroy);
|
||||
///
|
||||
/// #[derive(TypedPath)]
|
||||
/// #[typed_path("/users")]
|
||||
/// struct UsersCollection;
|
||||
///
|
||||
/// #[derive(Deserialize)]
|
||||
/// struct UsersCreatePayload { /* ... */ }
|
||||
///
|
||||
/// async fn users_create(
|
||||
/// _: UsersCollection,
|
||||
/// // Our handlers can accept other extractors.
|
||||
/// Json(payload): Json<UsersCreatePayload>,
|
||||
/// ) {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// async fn users_destroy(_: UsersCollection) { /* ... */ }
|
||||
///
|
||||
/// #
|
||||
/// # let app: Router<axum::body::Body> = app;
|
||||
/// ```
|
||||
///
|
||||
/// # Using `#[derive(TypedPath)]`
|
||||
///
|
||||
/// While `TypedPath` can be implemented manually, it's _highly_ recommended to derive it:
|
||||
///
|
||||
/// ```
|
||||
/// use serde::Deserialize;
|
||||
/// use axum_extra::routing::TypedPath;
|
||||
///
|
||||
/// #[derive(TypedPath, Deserialize)]
|
||||
/// #[typed_path("/users/:id")]
|
||||
/// struct UsersMember {
|
||||
/// id: u32,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The macro expands to:
|
||||
///
|
||||
/// - A `TypedPath` implementation.
|
||||
/// - 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 it's a unit struct.
|
||||
/// - 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. Note that the
|
||||
/// [`Display`] implementation for each field must return something that's compatible with its
|
||||
/// [`Deserialize`] implementation.
|
||||
///
|
||||
/// 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;
|
||||
/// use axum_extra::routing::TypedPath;
|
||||
///
|
||||
/// #[derive(TypedPath, Deserialize)]
|
||||
/// #[typed_path("/users/:id/teams/:team_id")]
|
||||
/// struct UsersMember {
|
||||
/// id: u32,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Unit and tuple structs are also supported:
|
||||
///
|
||||
/// ```
|
||||
/// use serde::Deserialize;
|
||||
/// use axum_extra::routing::TypedPath;
|
||||
///
|
||||
/// #[derive(TypedPath)]
|
||||
/// #[typed_path("/users")]
|
||||
/// struct UsersCollection;
|
||||
///
|
||||
/// #[derive(TypedPath, Deserialize)]
|
||||
/// #[typed_path("/users/:id")]
|
||||
/// struct UsersMember(u32);
|
||||
/// ```
|
||||
///
|
||||
/// ## Percent encoding
|
||||
///
|
||||
/// The generated [`Display`] implementation will automatically percent-encode the arguments:
|
||||
///
|
||||
/// ```
|
||||
/// use serde::Deserialize;
|
||||
/// use axum_extra::routing::TypedPath;
|
||||
///
|
||||
/// #[derive(TypedPath, Deserialize)]
|
||||
/// #[typed_path("/users/:id")]
|
||||
/// struct UsersMember {
|
||||
/// id: String,
|
||||
/// }
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// UsersMember {
|
||||
/// id: "foo bar".to_string(),
|
||||
/// }.to_string(),
|
||||
/// "/users/foo%20bar",
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [`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
|
||||
/// [`Deserialize`]: serde::Deserialize
|
||||
pub trait TypedPath: std::fmt::Display {
|
||||
/// The path with optional captures such as `/users/:id`.
|
||||
const PATH: &'static str;
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// implement [`TypedPath`].
|
||||
///
|
||||
/// 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,)*)
|
||||
where
|
||||
P: TypedPath
|
||||
{}
|
||||
|
||||
impl<P, $($ty,)*> Sealed for (P, $($ty,)*)
|
||||
where
|
||||
P: TypedPath
|
||||
{}
|
||||
};
|
||||
}
|
||||
|
||||
impl_first_element_is!();
|
||||
impl_first_element_is!(T1);
|
||||
impl_first_element_is!(T1, T2);
|
||||
impl_first_element_is!(T1, T2, T3);
|
||||
impl_first_element_is!(T1, T2, T3, T4);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
|
||||
impl_first_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
|
||||
Reference in New Issue
Block a user