From 4f087190a55b88ffa46998f78dc52fffdd1971a0 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 13 Feb 2022 14:41:05 +0100 Subject: [PATCH] checkpoint --- axum-extra/src/routing/typed_path.rs | 59 ++++++++++++++++++---- axum-macros/src/typed_path.rs | 75 +++++++++++++++++----------- examples/hello-world/src/main.rs | 47 +++++++++-------- 3 files changed, 120 insertions(+), 61 deletions(-) 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) +}