From 9f5dbfd83a8a405e01d5d7517d1442eab1f5a983 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 13 Feb 2022 21:10:56 +0100 Subject: [PATCH] Simplify things quite a bit --- axum-extra/Cargo.toml | 1 + axum-extra/src/routing/mod.rs | 137 ++++++++++++++++++++++++++- axum-extra/src/routing/typed.rs | 103 ++++++++++++++++++++ axum-extra/src/routing/typed_path.rs | 64 ------------- axum-macros/src/typed_path.rs | 43 +++++++-- axum/src/routing/mod.rs | 12 ++- examples/hello-world/src/main.rs | 16 +++- 7 files changed, 298 insertions(+), 78 deletions(-) create mode 100644 axum-extra/src/routing/typed.rs delete mode 100644 axum-extra/src/routing/typed_path.rs diff --git a/axum-extra/Cargo.toml b/axum-extra/Cargo.toml index ffc1926d..43093109 100644 --- a/axum-extra/Cargo.toml +++ b/axum-extra/Cargo.toml @@ -31,6 +31,7 @@ serde_json = { version = "1.0.71", optional = true } [dev-dependencies] axum-macros = { path = "../axum-macros", version = "0.1" } hyper = "0.14" +serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.14", features = ["full"] } tower = { version = "0.4", features = ["util"] } diff --git a/axum-extra/src/routing/mod.rs b/axum-extra/src/routing/mod.rs index 03c082c8..60a21882 100644 --- a/axum-extra/src/routing/mod.rs +++ b/axum-extra/src/routing/mod.rs @@ -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: sealed::Sealed { @@ -33,6 +36,62 @@ pub trait RouterExt: sealed::Sealed { fn with(self, routes: T) -> Self where T: HasRoutes; + + /// TODO(david): docs + fn typed_get(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath; + + /// TODO(david): docs + fn typed_delete(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath; + + /// TODO(david): docs + fn typed_head(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath; + + /// TODO(david): docs + fn typed_options(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath; + + /// TODO(david): docs + fn typed_patch(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath; + + /// TODO(david): docs + fn typed_post(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath; + + /// TODO(david): docs + fn typed_put(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath; + + /// TODO(david): docs + fn typed_trace(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath; } impl RouterExt for Router @@ -45,6 +104,78 @@ where { self.merge(routes.routes()) } + + fn typed_get(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::get(handler)) + } + + fn typed_delete(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::delete(handler)) + } + + fn typed_head(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::head(handler)) + } + + fn typed_options(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::options(handler)) + } + + fn typed_patch(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::patch(handler)) + } + + fn typed_post(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::post(handler)) + } + + fn typed_put(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::put(handler)) + } + + fn typed_trace(self, handler: H) -> Self + where + H: Handler, + T: FirstElementIs

+ 'static, + P: TypedPath, + { + self.route(P::PATH, axum::routing::trace(handler)) + } } /// Trait for things that can provide routes. diff --git a/axum-extra/src/routing/typed.rs b/axum-extra/src/routing/typed.rs new file mode 100644 index 00000000..9aa953de --- /dev/null +++ b/axum-extra/src/routing/typed.rs @@ -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, +/// ) { +/// // ... +/// } +/// +/// async fn users_destroy(_: UsersCollection) { /* ... */ } +/// +/// # +/// # let app: Router = 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

: Sealed {} + +macro_rules! impl_first_element_is { + ( $($ty:ident),* $(,)? ) => { + impl FirstElementIs

for (P, $($ty,)*) {} + + impl 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); diff --git a/axum-extra/src/routing/typed_path.rs b/axum-extra/src/routing/typed_path.rs deleted file mode 100644 index 369ba55f..00000000 --- a/axum-extra/src/routing/typed_path.rs +++ /dev/null @@ -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(handler: H) -> TypedPathRouter -where - H: Handler, - P: TypedPath, - T: FirstElementIs

+ 'static, - B: HttpBody + Send + 'static, -{ - TypedPathRouter { - router: Router::new().route(P::PATH, routing::get(handler)), - _path: PhantomData, - } -} - -pub struct TypedPathRouter { - router: Router, - _path: PhantomData

, -} - -impl TypedPathRouter -where - B: HttpBody + Send + 'static, - P: TypedPath, -{ - pub fn post(mut self, handler: H) -> Self - where - H: Handler, - T: FirstElementIs

+ 'static, - { - self.router = self.router.route(P::PATH, routing::post(handler)); - self - } -} - -impl HasRoutes for TypedPathRouter { - fn routes(self) -> Router { - self.router - } -} - -pub trait FirstElementIs

{} -impl

FirstElementIs

for (P,) {} -impl FirstElementIs

for (P, T1) {} -impl FirstElementIs

for (P, T1, T2) {} diff --git a/axum-macros/src/typed_path.rs b/axum-macros/src/typed_path.rs index eef66cc4..9d526370 100644 --- a/axum-macros/src/typed_path.rs +++ b/axum-macros/src/typed_path.rs @@ -43,7 +43,14 @@ fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result { for attr in attrs { if attr.path.is_ident("typed_path") { - path = Some(attr.parse_args()?); + if path.is_some() { + return Err(syn::Error::new_spanned( + attr, + "`typed_path` specified more than once", + )); + } else { + path = Some(attr.parse_args()?); + } } } @@ -65,10 +72,13 @@ fn expand_named_fields(ident: &syn::Ident, path: LitStr, segments: &[Segment]) - #[automatically_derived] impl ::axum_extra::routing::TypedPath for #ident { const PATH: &'static str = #path; + } - fn path(&self) -> ::std::borrow::Cow<'static, str> { + #[automatically_derived] + impl ::std::fmt::Display for #ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { let Self { #(#captures,)* } = self; - format!(#format_str, #(#captures = #captures,)*).into() + write!(f, #format_str, #(#captures = #captures,)*) } } @@ -137,10 +147,26 @@ fn expand_unnamed_fields( #[automatically_derived] impl ::axum_extra::routing::TypedPath for #ident { const PATH: &'static str = #path; + } - fn path(&self) -> ::std::borrow::Cow<'static, str> { + #[automatically_derived] + impl ::std::fmt::Display for #ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { let Self { #(#destructure_self)* } = self; - format!(#format_str, #(#captures = #captures,)*).into() + write!(f, #format_str, #(#captures = #captures,)*) + } + } + + #[::axum::async_trait] + #[automatically_derived] + impl ::axum::extract::FromRequest for #ident + where + B: Send, + { + type Rejection = <::axum::extract::Path as ::axum::extract::FromRequest>::Rejection; + + async fn from_request(req: &mut ::axum::extract::RequestParts) -> Result { + ::axum::extract::Path::from_request(req).await.map(|path| path.0) } } }) @@ -159,9 +185,12 @@ fn expand_unit_fields(ident: &syn::Ident, path: LitStr) -> TokenStream { #[automatically_derived] impl ::axum_extra::routing::TypedPath for #ident { const PATH: &'static str = #path; + } - fn path(&self) -> ::std::borrow::Cow<'static, str> { - #path.into() + #[automatically_derived] + impl ::std::fmt::Display for #ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, #path) } } diff --git a/axum/src/routing/mod.rs b/axum/src/routing/mod.rs index 931bcdbb..54bb3215 100644 --- a/axum/src/routing/mod.rs +++ b/axum/src/routing/mod.rs @@ -60,7 +60,6 @@ impl RouteId { } /// The router type for composing handlers and services. -#[derive(Debug)] pub struct Router { routes: HashMap>, node: Node, @@ -88,6 +87,17 @@ where } } +impl fmt::Debug for Router { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Router") + .field("routes", &self.routes) + .field("node", &self.node) + .field("fallback", &self.fallback) + .field("nested_at_root", &self.nested_at_root) + .finish() + } +} + pub(crate) const NEST_TAIL_PARAM: &str = "axum_nest"; const NEST_TAIL_PARAM_CAPTURE: &str = "/*axum_nest"; diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index 82968541..cdb93b84 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -7,15 +7,17 @@ // Just using this file for manual testing. Will be cleaned up before an eventual merge use axum::{response::IntoResponse, Router}; -use axum_extra::routing::{typed_path, RouterExt}; +use axum_extra::routing::RouterExt; use axum_macros::TypedPath; use serde::Deserialize; #[tokio::main] async fn main() { let app = Router::new() - .with(typed_path::get(users_index).post(users_create)) - .with(typed_path::get(users_show)); + .typed_get(users_index) + .typed_post(users_create) + .typed_get(users_show) + .typed_get(users_edit); axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()) .serve(app.into_make_service()) @@ -33,6 +35,10 @@ struct UsersMember { id: u32, } +#[derive(Deserialize, TypedPath)] +#[typed_path("/users/:id/edit")] +struct UsersEdit(u32); + async fn users_index(_: UsersCollection) -> impl IntoResponse { "users#index" } @@ -44,3 +50,7 @@ async fn users_create(_: UsersCollection, _payload: String) -> impl IntoResponse async fn users_show(UsersMember { id }: UsersMember) -> impl IntoResponse { format!("users#show: {}", id) } + +async fn users_edit(UsersEdit(id): UsersEdit) -> impl IntoResponse { + format!("users#edit: {}", id) +}