diff --git a/axum-extra/src/routing/typed_path.rs b/axum-extra/src/routing/typed_path.rs
index 38e47c99..369ba55f 100644
--- a/axum-extra/src/routing/typed_path.rs
+++ b/axum-extra/src/routing/typed_path.rs
@@ -1,8 +1,9 @@
-#![allow(missing_docs)]
+#![allow(missing_docs, missing_debug_implementations)]
-use axum::extract::{FromRequest, Path};
-use serde::de::DeserializeOwned;
-use std::borrow::Cow;
+use axum::{body::HttpBody, handler::Handler, routing, Router};
+use std::{borrow::Cow, marker::PhantomData};
+
+use super::HasRoutes;
/// ```rust
/// use axum_macros::TypedPath;
@@ -13,13 +14,51 @@ use std::borrow::Cow;
/// id: u32,
/// }
/// ```
-pub trait TypedPath: FromRequest + DeserializeOwned {
+pub trait TypedPath {
const PATH: &'static str;
-
fn path(&self) -> Cow<'static, str>;
}
-// pub trait FirstElementIsPath {}
-// impl
FirstElementIsPath for (Path
,) {}
-// impl
FirstElementIsPath for (Path
, T1) {}
-// impl
FirstElementIsPath for (Path
, T1, T2) {}
+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 59934358..eef66cc4 100644
--- a/axum-macros/src/typed_path.rs
+++ b/axum-macros/src/typed_path.rs
@@ -11,18 +11,25 @@ pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result {
..
} = &item_struct;
+ if !generics.params.is_empty() || generics.where_clause.is_some() {
+ return Err(syn::Error::new_spanned(
+ generics,
+ "`#[derive(TypePath)]` 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, generics, path, &segments))
+ Ok(expand_named_fields(ident, path, &segments))
}
syn::Fields::Unnamed(fields) => {
let segments = parse_path(&path);
- expand_unnamed_fields(fields, ident, generics, path, &segments)
+ expand_unnamed_fields(fields, ident, path, &segments)
}
- syn::Fields::Unit => Ok(expand_unit_fields(ident, generics, path)),
+ syn::Fields::Unit => Ok(expand_unit_fields(ident, path)),
}
}
@@ -50,22 +57,13 @@ fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result {
})
}
-fn expand_named_fields(
- ident: &syn::Ident,
- generics: &syn::Generics,
- path: LitStr,
- segments: &[Segment],
-) -> TokenStream {
- let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
-
+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);
quote_spanned! {path.span()=>
#[automatically_derived]
- impl #impl_generics ::axum_extra::routing::TypedPath for #ident
- #ty_generics #where_clause
- {
+ impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
fn path(&self) -> ::std::borrow::Cow<'static, str> {
@@ -73,18 +71,28 @@ fn expand_named_fields(
format!(#format_str, #(#captures = #captures,)*).into()
}
}
+
+ #[::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)
+ }
+ }
}
}
fn expand_unnamed_fields(
fields: &syn::FieldsUnnamed,
ident: &syn::Ident,
- generics: &syn::Generics,
path: LitStr,
segments: &[Segment],
) -> syn::Result {
- let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
-
let num_captures = segments
.iter()
.filter(|segment| match segment {
@@ -127,9 +135,7 @@ fn expand_unnamed_fields(
Ok(quote_spanned! {path.span()=>
#[automatically_derived]
- impl #impl_generics ::axum_extra::routing::TypedPath for #ident
- #ty_generics #where_clause
- {
+ impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
fn path(&self) -> ::std::borrow::Cow<'static, str> {
@@ -148,20 +154,33 @@ fn simple_pluralize(count: usize, word: &str) -> String {
}
}
-fn expand_unit_fields(ident: &syn::Ident, generics: &syn::Generics, path: LitStr) -> TokenStream {
- let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
-
+fn expand_unit_fields(ident: &syn::Ident, path: LitStr) -> TokenStream {
quote_spanned! {path.span()=>
#[automatically_derived]
- impl #impl_generics ::axum_extra::routing::TypedPath for #ident
- #ty_generics #where_clause
- {
+ impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
fn path(&self) -> ::std::borrow::Cow<'static, str> {
#path.into()
}
}
+
+ #[::axum::async_trait]
+ #[automatically_derived]
+ impl ::axum::extract::FromRequest for #ident
+ where
+ B: Send,
+ {
+ type Rejection = ::axum::http::StatusCode;
+
+ async fn from_request(req: &mut ::axum::extract::RequestParts) -> Result {
+ if req.uri().path() == ::PATH {
+ Ok(Self)
+ } else {
+ Err(::axum::http::StatusCode::NOT_FOUND)
+ }
+ }
+ }
}
}
@@ -180,9 +199,7 @@ fn captures_from_path(segments: &[Segment]) -> Vec {
segments
.iter()
.filter_map(|segment| match segment {
- Segment::Capture(capture, span) => {
- Some(format_ident!("{}", capture, span = span.clone()))
- }
+ Segment::Capture(capture, span) => Some(format_ident!("{}", capture, span = *span)),
Segment::Static(_) => None,
})
.collect::>()
diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs
index 0136dcd3..82968541 100644
--- a/examples/hello-world/src/main.rs
+++ b/examples/hello-world/src/main.rs
@@ -4,40 +4,43 @@
//! cargo run -p example-hello-world
//! ```
-use axum::{extract::Path, routing::get, Router};
-use axum_extra::routing::TypedPath;
+// 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_macros::TypedPath;
use serde::Deserialize;
-use std::net::SocketAddr;
#[tokio::main]
async fn main() {
- // build our application with a route
let app = Router::new()
- .route(UsersIndex::PATH, get(|_: Path| async {}))
- .route(UsersShow::PATH, get(|_: Path| async {}))
- .route(UsersEdit::PATH, get(|_: Path| async {}));
+ .with(typed_path::get(users_index).post(users_create))
+ .with(typed_path::get(users_show));
- // run it
- let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
- println!("listening on {}", addr);
- axum::Server::bind(&addr)
+ axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
-#[derive(Deserialize, TypedPath)]
+#[derive(TypedPath)]
#[typed_path("/users")]
-struct UsersIndex;
+struct UsersCollection;
-// #[derive(Deserialize, TypedPath)]
-// #[typed_path("/users/:id/teams/:team_id")]
-// struct UsersShow {
-// id: u32,
-// team_id: u32,
-// }
+#[derive(Deserialize, TypedPath)]
+#[typed_path("/users/:id")]
+struct UsersMember {
+ id: u32,
+}
-// #[derive(Deserialize, TypedPath)]
-// #[typed_path("/users/:id/edit")]
-// struct UsersEdit(u32);
+async fn users_index(_: UsersCollection) -> impl IntoResponse {
+ "users#index"
+}
+
+async fn users_create(_: UsersCollection, _payload: String) -> impl IntoResponse {
+ "users#create"
+}
+
+async fn users_show(UsersMember { id }: UsersMember) -> impl IntoResponse {
+ format!("users#show: {}", id)
+}