checkpoint

This commit is contained in:
David Pedersen
2022-02-13 14:41:49 +01:00
parent 47f5954903
commit 4f087190a5
3 changed files with 120 additions and 61 deletions
+49 -10
View File
@@ -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<B>: FromRequest<B> + DeserializeOwned {
pub trait TypedPath {
const PATH: &'static str;
fn path(&self) -> Cow<'static, str>;
}
// pub trait FirstElementIsPath {}
// impl<P> FirstElementIsPath for (Path<P>,) {}
// impl<P, T1> FirstElementIsPath for (Path<P>, T1) {}
// impl<P, T1, T2> FirstElementIsPath for (Path<P>, T1, T2) {}
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) {}
+46 -29
View File
@@ -11,18 +11,25 @@ pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
..
} = &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<Attrs> {
})
}
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<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)
}
}
}
}
fn expand_unnamed_fields(
fields: &syn::FieldsUnnamed,
ident: &syn::Ident,
generics: &syn::Generics,
path: LitStr,
segments: &[Segment],
) -> syn::Result<TokenStream> {
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<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)
}
}
}
}
}
@@ -180,9 +199,7 @@ 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.clone()))
}
Segment::Capture(capture, span) => Some(format_ident!("{}", capture, span = *span)),
Segment::Static(_) => None,
})
.collect::<Vec<_>>()
+25 -22
View File
@@ -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<UsersIndex>| async {}))
.route(UsersShow::PATH, get(|_: Path<UsersShow>| async {}))
.route(UsersEdit::PATH, get(|_: Path<UsersEdit>| 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)
}