Simplify things quite a bit

This commit is contained in:
David Pedersen
2022-02-13 21:10:56 +01:00
parent 4f087190a5
commit 9f5dbfd83a
7 changed files with 298 additions and 78 deletions
+134 -3
View File
@@ -1,11 +1,14 @@
//! Additional types for defining routes.
use axum::{body::Body, Router};
use axum::{body::Body, handler::Handler, Router};
mod resource;
pub mod typed_path;
mod typed;
pub use self::{resource::Resource, typed_path::TypedPath};
pub use self::{
resource::Resource,
typed::{FirstElementIs, TypedPath},
};
/// Extension trait that adds additional methods to [`Router`].
pub trait RouterExt<B>: sealed::Sealed {
@@ -33,6 +36,62 @@ pub trait RouterExt<B>: sealed::Sealed {
fn with<T>(self, routes: T) -> Self
where
T: HasRoutes<B>;
/// TODO(david): docs
fn typed_get<H, T, P>(self, handler: H) -> Self
where
H: Handler<T, B>,
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
fn typed_delete<H, T, P>(self, handler: H) -> Self
where
H: Handler<T, B>,
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
fn typed_head<H, T, P>(self, handler: H) -> Self
where
H: Handler<T, B>,
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
fn typed_options<H, T, P>(self, handler: H) -> Self
where
H: Handler<T, B>,
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
fn typed_patch<H, T, P>(self, handler: H) -> Self
where
H: Handler<T, B>,
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
fn typed_post<H, T, P>(self, handler: H) -> Self
where
H: Handler<T, B>,
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
fn typed_put<H, T, P>(self, handler: H) -> Self
where
H: Handler<T, B>,
T: FirstElementIs<P> + 'static,
P: TypedPath;
/// TODO(david): docs
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>
@@ -45,6 +104,78 @@ where
{
self.merge(routes.routes())
}
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))
}
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))
}
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))
}
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))
}
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))
}
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))
}
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))
}
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.
+103
View File
@@ -0,0 +1,103 @@
/// Type safe routing.
///
/// # Example
///
/// ```rust
/// use serde::Deserialize;
/// use axum_macros::TypedPath;
/// use axum::{Router, extract::Json};
/// use axum_extra::routing::{
/// typed,
/// RouterExt, // for `Router::with`
/// };
///
/// // A type safe route with `/users/:id` as its associated path.
/// #[derive(Deserialize, TypedPath)]
/// #[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.
/// //
/// // 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`
/// .with(typed::get(users_show))
/// // Add multiple handlers for `/users` depending on the HTTP method.
/// .with(typed::post(users_create).delete(users_destroy))
/// // We can still add regular routes.
/// .route("/foo", get(|| async { /* ... */ }));
///
/// #[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<Payload>,
/// ) {
/// // ...
/// }
///
/// async fn users_destroy(_: UsersCollection) { /* ... */ }
///
/// #
/// # let app: Router<axum::body::Body> = app;
/// ```
use super::sealed::Sealed;
pub trait TypedPath: std::fmt::Display {
const PATH: &'static str;
}
/// Utility trait used with [`TypedRouter`] 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.
pub trait FirstElementIs<P>: Sealed {}
macro_rules! impl_first_element_is {
( $($ty:ident),* $(,)? ) => {
impl<P, $($ty,)*> FirstElementIs<P> for (P, $($ty,)*) {}
impl<P, $($ty,)*> Sealed for (P, $($ty,)*) {}
};
}
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);
-64
View File
@@ -1,64 +0,0 @@
#![allow(missing_docs, missing_debug_implementations)]
use axum::{body::HttpBody, handler::Handler, routing, Router};
use std::{borrow::Cow, marker::PhantomData};
use super::HasRoutes;
/// ```rust
/// use axum_macros::TypedPath;
///
/// #[derive(TypedPath)]
/// #[typed_path("/users/:id")]
/// struct UsersShow {
/// id: u32,
/// }
/// ```
pub trait TypedPath {
const PATH: &'static str;
fn path(&self) -> Cow<'static, str>;
}
pub fn get<H, B, T, P>(handler: H) -> TypedPathRouter<P, B>
where
H: Handler<T, B>,
P: TypedPath,
T: FirstElementIs<P> + 'static,
B: HttpBody + Send + 'static,
{
TypedPathRouter {
router: Router::new().route(P::PATH, routing::get(handler)),
_path: PhantomData,
}
}
pub struct TypedPathRouter<P, B> {
router: Router<B>,
_path: PhantomData<P>,
}
impl<P, B> TypedPathRouter<P, B>
where
B: HttpBody + Send + 'static,
P: TypedPath,
{
pub fn post<H, T>(mut self, handler: H) -> Self
where
H: Handler<T, B>,
T: FirstElementIs<P> + 'static,
{
self.router = self.router.route(P::PATH, routing::post(handler));
self
}
}
impl<P, B> HasRoutes<B> for TypedPathRouter<P, B> {
fn routes(self) -> Router<B> {
self.router
}
}
pub trait FirstElementIs<P> {}
impl<P> FirstElementIs<P> for (P,) {}
impl<P, T1> FirstElementIs<P> for (P, T1) {}
impl<P, T1, T2> FirstElementIs<P> for (P, T1, T2) {}