mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-24 00:00:16 +02:00
make macro implement trait
This commit is contained in:
@@ -3,8 +3,9 @@
|
||||
use axum::{body::Body, Router};
|
||||
|
||||
mod resource;
|
||||
mod typed_path;
|
||||
|
||||
pub use self::resource::Resource;
|
||||
pub use self::{resource::Resource, typed_path::TypedPath};
|
||||
|
||||
/// Extension trait that adds additional methods to [`Router`].
|
||||
pub trait RouterExt<B>: sealed::Sealed {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// TODO
|
||||
pub trait TypedPath {
|
||||
/// TODO
|
||||
const PATH: &'static str;
|
||||
|
||||
/// TODO
|
||||
fn path(&self) -> Cow<'static, str>;
|
||||
}
|
||||
@@ -49,7 +49,7 @@ use syn::parse::Parse;
|
||||
|
||||
mod debug_handler;
|
||||
mod from_request;
|
||||
mod uri;
|
||||
mod typed_path;
|
||||
|
||||
/// Derive an implementation of [`FromRequest`].
|
||||
///
|
||||
@@ -387,9 +387,9 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
}
|
||||
|
||||
/// TODO
|
||||
#[proc_macro_derive(Uri, attributes(uri))]
|
||||
pub fn derive_uri(input: TokenStream) -> TokenStream {
|
||||
expand_with(input, route::expand)
|
||||
#[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
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{ItemStruct, LitStr};
|
||||
|
||||
pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
|
||||
let ItemStruct {
|
||||
attrs,
|
||||
vis: _,
|
||||
struct_token: _,
|
||||
ident,
|
||||
generics,
|
||||
fields,
|
||||
semi_token: _,
|
||||
} = &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, 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)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Attrs {
|
||||
path: String,
|
||||
}
|
||||
|
||||
fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result<Attrs> {
|
||||
let mut path = None::<String>;
|
||||
|
||||
for attr in attrs {
|
||||
if attr.path.is_ident("typed_path") {
|
||||
path = Some(attr.parse_args::<LitStr>()?.value());
|
||||
}
|
||||
}
|
||||
|
||||
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: String, segments: &[Segment]) -> TokenStream {
|
||||
let format_str = format_str_from_path(segments);
|
||||
let captures = captures_from_path(segments);
|
||||
|
||||
quote! {
|
||||
#[automatically_derived]
|
||||
impl ::axum_extra::routing::TypedPath for #ident {
|
||||
const PATH: &'static str = #path;
|
||||
|
||||
fn path(&self) -> ::std::borrow::Cow<'static, str> {
|
||||
let Self { #(#captures,)* } = self;
|
||||
format!(#format_str, #(#captures = #captures,)*).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_unnamed_fields(
|
||||
fields: &syn::FieldsUnnamed,
|
||||
ident: &syn::Ident,
|
||||
path: String,
|
||||
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);
|
||||
quote! {
|
||||
#idx: #capture,
|
||||
}
|
||||
});
|
||||
|
||||
let format_str = format_str_from_path(segments);
|
||||
let captures = captures_from_path(segments);
|
||||
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl ::axum_extra::routing::TypedPath for #ident {
|
||||
const PATH: &'static str = #path;
|
||||
|
||||
fn path(&self) -> ::std::borrow::Cow<'static, str> {
|
||||
let Self { #(#destructure_self)* } = self;
|
||||
format!(#format_str, #(#captures = #captures,)*).into()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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: String) -> TokenStream {
|
||||
quote! {
|
||||
#[automatically_derived]
|
||||
impl ::axum_extra::routing::TypedPath for #ident {
|
||||
const PATH: &'static str = #path;
|
||||
|
||||
fn path(&self) -> ::std::borrow::Cow<'static, str> {
|
||||
#path.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) => Some(format_ident!("{}", capture)),
|
||||
Segment::Static(_) => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn parse_path(path: &str) -> Vec<Segment> {
|
||||
path.split('/')
|
||||
.map(|segment| {
|
||||
if let Some(capture) = segment.strip_prefix(':') {
|
||||
Segment::Capture(capture.to_owned())
|
||||
} else {
|
||||
Segment::Static(segment.to_owned())
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
enum Segment {
|
||||
Capture(String),
|
||||
Static(String),
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use std::borrow::Cow;
|
||||
use syn::{spanned::Spanned, ItemStruct, LitStr};
|
||||
|
||||
pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
|
||||
let ItemStruct {
|
||||
attrs,
|
||||
vis,
|
||||
struct_token: _,
|
||||
ident,
|
||||
generics,
|
||||
fields,
|
||||
semi_token: _,
|
||||
} = &item_struct;
|
||||
|
||||
if !generics.params.is_empty() || generics.where_clause.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
generics,
|
||||
"`#[derive(Path)]` doesn't support generics",
|
||||
));
|
||||
}
|
||||
|
||||
let Attrs { route } = parse_attrs(attrs)?;
|
||||
|
||||
let route_method = route_method(&route, vis, fields);
|
||||
let from_request_impl = from_request_impl(ident, fields);
|
||||
|
||||
Ok(quote! {
|
||||
#[automatically_derived]
|
||||
impl #ident {
|
||||
#vis const ROUTE: &'static str = #route;
|
||||
|
||||
#route_method
|
||||
}
|
||||
|
||||
#from_request_impl
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Attrs {
|
||||
route: String,
|
||||
}
|
||||
|
||||
fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result<Attrs> {
|
||||
let mut route = None::<String>;
|
||||
|
||||
for attr in attrs {
|
||||
if attr.path.is_ident("uri") {
|
||||
route = Some(attr.parse_args::<LitStr>()?.value());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Attrs {
|
||||
route: route.ok_or_else(|| {
|
||||
syn::Error::new(Span::call_site(), "missing `#[route(\"...\")]` attribute")
|
||||
})?,
|
||||
})
|
||||
}
|
||||
|
||||
fn route_method(route: &str, vis: &syn::Visibility, fields: &syn::Fields) -> TokenStream {
|
||||
let format_str = path_into_format_str(route, matches!(fields, syn::Fields::Unnamed(_)));
|
||||
|
||||
match fields {
|
||||
syn::Fields::Named(fields) => {
|
||||
let set_placeholders = fields.named.iter().map(|field| {
|
||||
let ident = &field.ident;
|
||||
quote! { #ident = self.#ident }
|
||||
});
|
||||
|
||||
quote! {
|
||||
#vis fn path(&self) -> ::std::borrow::Cow<'static, str> {
|
||||
format!(#format_str, #(#set_placeholders,)*).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
syn::Fields::Unnamed(fields) => {
|
||||
let set_placeholders = fields.unnamed.iter().enumerate().map(|(index, field)| {
|
||||
let field = syn::Member::Unnamed(syn::Index {
|
||||
index: index as _,
|
||||
span: field.span(),
|
||||
});
|
||||
quote! { self.#field }
|
||||
});
|
||||
|
||||
quote! {
|
||||
#vis fn path(&self) -> ::std::borrow::Cow<'static, str> {
|
||||
format!(#format_str, #(#set_placeholders,)*).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
syn::Fields::Unit => quote! {
|
||||
#vis fn uri(&self) -> ::std::borrow::Cow<'static, str> {
|
||||
#route.into()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn path_into_format_str(route: &str, index_placeholders: bool) -> String {
|
||||
let mut index = 0;
|
||||
|
||||
route
|
||||
.split('/')
|
||||
.map(|segment| {
|
||||
if let Some(capture) = segment.strip_prefix(':') {
|
||||
if index_placeholders {
|
||||
let segment = Cow::Owned(format!("{{{}}}", index));
|
||||
index += 1;
|
||||
segment
|
||||
} else {
|
||||
Cow::Owned(format!("{{{}}}", capture))
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(segment)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
fn from_request_impl(ident: &syn::Ident, fields: &syn::Fields) -> TokenStream {
|
||||
let rejection = match fields {
|
||||
syn::Fields::Named(_) | syn::Fields::Unnamed(_) => quote! {
|
||||
<::axum::extract::Path<Self> as ::axum::extract::FromRequest<B>>::Rejection
|
||||
},
|
||||
syn::Fields::Unit => quote! { ::std::convert::Infallible },
|
||||
};
|
||||
|
||||
let from_request_body = match fields {
|
||||
syn::Fields::Named(_) | syn::Fields::Unnamed(_) => quote! {
|
||||
::axum::extract::FromRequest::from_request(req)
|
||||
.await
|
||||
.map(|::axum::extract::Path(inner)| inner)
|
||||
},
|
||||
syn::Fields::Unit => quote! {
|
||||
Ok(Self)
|
||||
},
|
||||
};
|
||||
|
||||
quote! {
|
||||
#[::axum::async_trait]
|
||||
#[automatically_derived]
|
||||
impl<B> ::axum::extract::FromRequest<B> for #ident
|
||||
where
|
||||
B: Send,
|
||||
{
|
||||
type Rejection = #rejection;
|
||||
|
||||
async fn from_request(
|
||||
req: &mut ::axum::extract::RequestParts<B>,
|
||||
) -> ::std::result::Result<Self, Self::Rejection> {
|
||||
#from_request_body
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,5 @@ axum = { path = "../../axum" }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
|
||||
axum-macros = { path = "../../axum-macros" }
|
||||
axum-extra = { path = "../../axum-extra" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
//! cargo run -p example-hello-world
|
||||
//! ```
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use axum::{response::Html, routing::get, Router};
|
||||
use axum_macros::Uri;
|
||||
use axum::{extract::Path, routing::get, Router};
|
||||
use axum_extra::routing::TypedPath;
|
||||
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("/", get(handler));
|
||||
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 {}));
|
||||
|
||||
// run it
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
@@ -25,21 +27,17 @@ async fn main() {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn handler(_: UsersShow) -> Html<&'static str> {
|
||||
Html("<h1>Hello, World!</h1>")
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Uri)]
|
||||
#[uri("/users")]
|
||||
#[derive(Deserialize, TypedPath)]
|
||||
#[typed_path("/users")]
|
||||
struct UsersIndex;
|
||||
|
||||
#[derive(Deserialize, Uri)]
|
||||
#[uri("/users/:id/teams/:team_id")]
|
||||
#[derive(Deserialize, TypedPath)]
|
||||
#[typed_path("/users/:id/teams/:team_id")]
|
||||
struct UsersShow {
|
||||
id: u32,
|
||||
team_id: u32,
|
||||
}
|
||||
|
||||
// #[derive(serde::Deserialize)]
|
||||
// #[route("/users/:id/edit")]
|
||||
// struct UsersEdit(u32);
|
||||
#[derive(Deserialize, TypedPath)]
|
||||
#[typed_path("/users/:id/edit")]
|
||||
struct UsersEdit(u32);
|
||||
|
||||
Reference in New Issue
Block a user