1
0
Fork 0
mirror of https://github.com/tokio-rs/axum.git synced 2025-04-26 13:56:22 +02:00

Type safe routing ()

* 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 <jplatte@users.noreply.github.com>

* add note about Display/Serialize being compatible

* Update axum-extra/src/routing/typed.rs

Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>

* 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 <jplatte@users.noreply.github.com>

* rephrase args

* changelog

Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
This commit is contained in:
David Pedersen 2022-02-18 14:13:56 +01:00 committed by GitHub
parent d12494cc9c
commit 7a228a584b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 929 additions and 6 deletions

View file

@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# Unreleased
- **added:** Add type safe routing. See `axum_extra::routing::typed` for more details ([#756])
- **breaking:** `CachedRejection` has been removed ([#699])
- **breaking:** `<Cached<T> as FromRequest>::Rejection` is now `T::Rejection`. ([#699])
- **breaking:** `middleware::from_fn` has been moved into the main axum crate ([#719])
@ -14,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#666]: https://github.com/tokio-rs/axum/pull/666
[#699]: https://github.com/tokio-rs/axum/pull/699
[#719]: https://github.com/tokio-rs/axum/pull/719
[#756]: https://github.com/tokio-rs/axum/pull/756
# 0.1.2 (13. January, 2021)

View file

@ -11,7 +11,9 @@ repository = "https://github.com/tokio-rs/axum"
version = "0.1.2"
[features]
erased-json = ["serde", "serde_json"]
default = []
erased-json = ["serde_json", "serde"]
typed-routing = ["axum-macros", "serde", "percent-encoding"]
[dependencies]
axum = { path = "../axum", version = "0.4" }
@ -25,11 +27,14 @@ tower-layer = "0.3"
tower-service = "0.3"
# optional dependencies
serde = { version = "1.0.130", optional = true }
axum-macros = { path = "../axum-macros", version = "0.1", optional = true }
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0.71", optional = true }
percent-encoding = { version = "2.1", optional = true }
[dev-dependencies]
hyper = "0.14"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.14", features = ["full"] }
tower = { version = "0.4", features = ["util"] }

View file

@ -40,9 +40,24 @@
#![deny(unreachable_pub, private_in_public)]
#![allow(elided_lifetimes_in_paths, clippy::type_complexity)]
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
#![cfg_attr(test, allow(clippy::float_cmp))]
pub mod extract;
pub mod response;
pub mod routing;
#[cfg(feature = "typed-routing")]
#[doc(hidden)]
pub mod __private {
//! _not_ public API
use percent_encoding::{AsciiSet, CONTROLS};
pub use percent_encoding::utf8_percent_encode;
// from https://github.com/servo/rust-url/blob/master/url/src/parser.rs
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
}

View file

@ -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.

View file

@ -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);

View file

@ -7,8 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
# Unreleased
- None.
- Add `#[derive(TypedPath)]` for use with axum-extra's new "type safe" routing API ([#756])
# 0.1.0 (31. January, 2022)
- Initial release.
[#756]: https://github.com/tokio-rs/axum/pull/756

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", features = ["typed-routing"] }
rustversion = "1.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }

View file

@ -49,6 +49,7 @@ use syn::parse::Parse;
mod debug_handler;
mod from_request;
mod typed_path;
/// Derive an implementation of [`FromRequest`].
///
@ -385,6 +386,16 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
return expand_attr_with(_attr, input, debug_handler::expand);
}
/// Derive an implementation of [`axum_extra::routing::TypedPath`].
///
/// See that trait for more details.
///
/// [`axum_extra::routing::TypedPath`]: https://docs.rs/axum-extra/latest/axum_extra/routing/trait.TypedPath.html
#[proc_macro_derive(TypedPath, attributes(typed_path))]
pub fn derive_typed_path(input: TokenStream) -> TokenStream {
expand_with(input, typed_path::expand)
}
fn expand_with<F, I, K>(input: TokenStream, f: F) -> TokenStream
where
F: FnOnce(I) -> syn::Result<K>,

View file

@ -0,0 +1,324 @@
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::{ItemStruct, LitStr};
pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
let ItemStruct {
attrs,
ident,
generics,
fields,
..
} = &item_struct;
if !generics.params.is_empty() || generics.where_clause.is_some() {
return Err(syn::Error::new_spanned(
generics,
"`#[derive(TypedPath)]` doesn't support generics",
));
}
let Attrs { path } = parse_attrs(attrs)?;
match fields {
syn::Fields::Named(_) => {
let segments = parse_path(&path)?;
Ok(expand_named_fields(ident, path, &segments))
}
syn::Fields::Unnamed(fields) => {
let segments = parse_path(&path)?;
expand_unnamed_fields(fields, ident, path, &segments)
}
syn::Fields::Unit => Ok(expand_unit_fields(ident, path)?),
}
}
struct Attrs {
path: LitStr,
}
fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result<Attrs> {
let mut path = None;
for attr in attrs {
if attr.path.is_ident("typed_path") {
if path.is_some() {
return Err(syn::Error::new_spanned(
attr,
"`typed_path` specified more than once",
));
} else {
path = Some(attr.parse_args()?);
}
}
}
Ok(Attrs {
path: path.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"missing `#[typed_path(\"...\")]` attribute",
)
})?,
})
}
fn expand_named_fields(ident: &syn::Ident, path: LitStr, segments: &[Segment]) -> TokenStream {
let format_str = format_str_from_path(segments);
let captures = captures_from_path(segments);
let typed_path_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
}
};
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let Self { #(#captures,)* } = self;
write!(
f,
#format_str,
#(#captures = ::axum_extra::__private::utf8_percent_encode(&#captures.to_string(), ::axum_extra::__private::PATH_SEGMENT)),*
)
}
}
};
let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived]
impl<B> ::axum::extract::FromRequest<B> for #ident
where
B: Send,
{
type Rejection = <::axum::extract::Path<Self> as ::axum::extract::FromRequest<B>>::Rejection;
async fn from_request(req: &mut ::axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
::axum::extract::Path::from_request(req).await.map(|path| path.0)
}
}
};
quote! {
#typed_path_impl
#display_impl
#from_request_impl
}
}
fn expand_unnamed_fields(
fields: &syn::FieldsUnnamed,
ident: &syn::Ident,
path: LitStr,
segments: &[Segment],
) -> syn::Result<TokenStream> {
let num_captures = segments
.iter()
.filter(|segment| match segment {
Segment::Capture(_, _) => true,
Segment::Static(_) => false,
})
.count();
let num_fields = fields.unnamed.len();
if num_fields != num_captures {
return Err(syn::Error::new_spanned(
fields,
format!(
"Mismatch in number of captures and fields. Path has {} but struct has {}",
simple_pluralize(num_captures, "capture"),
simple_pluralize(num_fields, "field"),
),
));
}
let destructure_self = segments
.iter()
.filter_map(|segment| match segment {
Segment::Capture(capture, _) => Some(capture),
Segment::Static(_) => None,
})
.enumerate()
.map(|(idx, capture)| {
let idx = syn::Index {
index: idx as _,
span: Span::call_site(),
};
let capture = format_ident!("{}", capture, span = path.span());
quote_spanned! {path.span()=>
#idx: #capture,
}
});
let format_str = format_str_from_path(segments);
let captures = captures_from_path(segments);
let typed_path_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
}
};
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let Self { #(#destructure_self)* } = self;
write!(
f,
#format_str,
#(#captures = ::axum_extra::__private::utf8_percent_encode(&#captures.to_string(), ::axum_extra::__private::PATH_SEGMENT)),*
)
}
}
};
let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived]
impl<B> ::axum::extract::FromRequest<B> for #ident
where
B: Send,
{
type Rejection = <::axum::extract::Path<Self> as ::axum::extract::FromRequest<B>>::Rejection;
async fn from_request(req: &mut ::axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
::axum::extract::Path::from_request(req).await.map(|path| path.0)
}
}
};
Ok(quote! {
#typed_path_impl
#display_impl
#from_request_impl
})
}
fn simple_pluralize(count: usize, word: &str) -> String {
if count == 1 {
format!("{} {}", count, word)
} else {
format!("{} {}s", count, word)
}
}
fn expand_unit_fields(ident: &syn::Ident, path: LitStr) -> syn::Result<TokenStream> {
for segment in parse_path(&path)? {
match segment {
Segment::Capture(_, span) => {
return Err(syn::Error::new(
span,
"Typed paths for unit structs cannot contain captures",
));
}
Segment::Static(_) => {}
}
}
let typed_path_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
}
};
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, #path)
}
}
};
let from_request_impl = quote! {
#[::axum::async_trait]
#[automatically_derived]
impl<B> ::axum::extract::FromRequest<B> for #ident
where
B: Send,
{
type Rejection = ::axum::http::StatusCode;
async fn from_request(req: &mut ::axum::extract::RequestParts<B>) -> Result<Self, Self::Rejection> {
if req.uri().path() == <Self as ::axum_extra::routing::TypedPath>::PATH {
Ok(Self)
} else {
Err(::axum::http::StatusCode::NOT_FOUND)
}
}
}
};
Ok(quote! {
#typed_path_impl
#display_impl
#from_request_impl
})
}
fn format_str_from_path(segments: &[Segment]) -> String {
segments
.iter()
.map(|segment| match segment {
Segment::Capture(capture, _) => format!("{{{}}}", capture),
Segment::Static(segment) => segment.to_owned(),
})
.collect::<Vec<_>>()
.join("/")
}
fn captures_from_path(segments: &[Segment]) -> Vec<syn::Ident> {
segments
.iter()
.filter_map(|segment| match segment {
Segment::Capture(capture, span) => Some(format_ident!("{}", capture, span = *span)),
Segment::Static(_) => None,
})
.collect::<Vec<_>>()
}
fn parse_path(path: &LitStr) -> syn::Result<Vec<Segment>> {
path.value()
.split('/')
.map(|segment| {
if segment.contains('*') {
return Err(syn::Error::new_spanned(
path,
"`typed_path` cannot contain wildcards",
));
}
if let Some(capture) = segment.strip_prefix(':') {
Ok(Segment::Capture(capture.to_owned(), path.span()))
} else {
Ok(Segment::Static(segment.to_owned()))
}
})
.collect()
}
enum Segment {
Capture(String, Span),
Static(String),
}
#[test]
fn ui() {
#[rustversion::stable]
fn go() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/typed_path/fail/*.rs");
t.pass("tests/typed_path/pass/*.rs");
}
#[rustversion::not(stable)]
fn go() {}
go();
}

View file

@ -0,0 +1,10 @@
use axum_macros::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users")]
struct MyPath {
id: u32,
}
fn main() {}

View file

@ -0,0 +1,14 @@
error[E0027]: pattern does not mention field `id`
--> tests/typed_path/fail/missing_capture.rs:5:14
|
5 | #[typed_path("/users")]
| ^^^^^^^^ missing field `id`
|
help: include the missing field in the pattern
|
5 | #[typed_path("/users" { id })]
| ++++++
help: if you don't care about this missing field, you can explicitly ignore it
|
5 | #[typed_path("/users" { .. })]
| ++++++

View file

@ -0,0 +1,9 @@
use axum_macros::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
struct MyPath {}
fn main() {
}

View file

@ -0,0 +1,5 @@
error[E0026]: struct `MyPath` does not have a field named `id`
--> tests/typed_path/fail/missing_field.rs:5:14
|
5 | #[typed_path("/users/:id")]
| ^^^^^^^^^^^^ struct `MyPath` does not have this field

View file

@ -0,0 +1,9 @@
use axum_macros::TypedPath;
#[derive(TypedPath)]
#[typed_path("/users/:id")]
struct MyPath {
id: u32,
}
fn main() {}

View file

@ -0,0 +1,9 @@
error[E0277]: the trait bound `for<'de> MyPath: serde::de::Deserialize<'de>` is not satisfied
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
3 | #[derive(TypedPath)]
| ^^^^^^^^^ the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for `MyPath`
|
= note: required because of the requirements on the impl of `serde::de::DeserializeOwned` for `MyPath`
= note: required because of the requirements on the impl of `FromRequest<B>` for `axum::extract::Path<MyPath>`
= note: this error originates in the derive macro `TypedPath` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,8 @@
use axum_macros::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:id")]
struct MyPath;
fn main() {}

View file

@ -0,0 +1,5 @@
error: Typed paths for unit structs cannot contain captures
--> tests/typed_path/fail/unit_with_capture.rs:5:14
|
5 | #[typed_path("/users/:id")]
| ^^^^^^^^^^^^

View file

@ -0,0 +1,7 @@
use axum_extra::routing::TypedPath;
#[derive(TypedPath)]
#[typed_path("/users/*rest")]
struct MyPath;
fn main() {}

View file

@ -0,0 +1,5 @@
error: `typed_path` cannot contain wildcards
--> tests/typed_path/fail/wildcard.rs:4:14
|
4 | #[typed_path("/users/*rest")]
| ^^^^^^^^^^^^^^

View file

@ -0,0 +1,25 @@
use axum_extra::routing::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:user_id/teams/:team_id")]
struct MyPath {
user_id: u32,
team_id: u32,
}
fn main() {
axum::Router::<axum::body::Body>::new().route("/", axum::routing::get(|_: MyPath| async {}));
assert_eq!(MyPath::PATH, "/users/:user_id/teams/:team_id");
assert_eq!(
format!(
"{}",
MyPath {
user_id: 1,
team_id: 2
}
),
"/users/1/teams/2"
);
}

View file

@ -0,0 +1,13 @@
use axum_extra::routing::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/:user_id/teams/:team_id")]
struct MyPath(u32, u32);
fn main() {
axum::Router::<axum::body::Body>::new().route("/", axum::routing::get(|_: MyPath| async {}));
assert_eq!(MyPath::PATH, "/users/:user_id/teams/:team_id");
assert_eq!(format!("{}", MyPath(1, 2)), "/users/1/teams/2");
}

View file

@ -0,0 +1,13 @@
use axum_extra::routing::TypedPath;
#[derive(TypedPath)]
#[typed_path("/users")]
struct MyPath;
fn main() {
axum::Router::<axum::body::Body>::new()
.route("/", axum::routing::get(|_: MyPath| async {}));
assert_eq!(MyPath::PATH, "/users");
assert_eq!(format!("{}", MyPath), "/users");
}

View file

@ -0,0 +1,32 @@
use axum_extra::routing::TypedPath;
use serde::Deserialize;
#[derive(TypedPath, Deserialize)]
#[typed_path("/:param")]
struct Named {
param: String,
}
#[derive(TypedPath, Deserialize)]
#[typed_path("/:param")]
struct Unnamed(String);
fn main() {
assert_eq!(
format!(
"{}",
Named {
param: "a b".to_string()
}
),
"/a%20b"
);
assert_eq!(
format!(
"{}",
Unnamed("a b".to_string()),
),
"/a%20b"
);
}

View file

@ -60,7 +60,6 @@ impl RouteId {
}
/// The router type for composing handlers and services.
#[derive(Debug)]
pub struct Router<B = Body> {
routes: HashMap<RouteId, Endpoint<B>>,
node: Node,
@ -88,6 +87,17 @@ where
}
}
impl<B> fmt::Debug for Router<B> {
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";