diff --git a/axum-extra/src/routing/mod.rs b/axum-extra/src/routing/mod.rs index 112b67f4..7fdbf0c1 100644 --- a/axum-extra/src/routing/mod.rs +++ b/axum-extra/src/routing/mod.rs @@ -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: sealed::Sealed { diff --git a/axum-extra/src/routing/typed_path.rs b/axum-extra/src/routing/typed_path.rs new file mode 100644 index 00000000..2f1dcb6e --- /dev/null +++ b/axum-extra/src/routing/typed_path.rs @@ -0,0 +1,10 @@ +use std::borrow::Cow; + +/// TODO +pub trait TypedPath { + /// TODO + const PATH: &'static str; + + /// TODO + fn path(&self) -> Cow<'static, str>; +} diff --git a/axum-macros/src/lib.rs b/axum-macros/src/lib.rs index 7e00d90d..244766bd 100644 --- a/axum-macros/src/lib.rs +++ b/axum-macros/src/lib.rs @@ -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(input: TokenStream, f: F) -> TokenStream diff --git a/axum-macros/src/typed_path.rs b/axum-macros/src/typed_path.rs new file mode 100644 index 00000000..b324067a --- /dev/null +++ b/axum-macros/src/typed_path.rs @@ -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 { + 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 { + let mut path = None::; + + for attr in attrs { + if attr.path.is_ident("typed_path") { + path = Some(attr.parse_args::()?.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 { + 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::>() + .join("/") +} + +fn captures_from_path(segments: &[Segment]) -> Vec { + segments + .iter() + .filter_map(|segment| match segment { + Segment::Capture(capture) => Some(format_ident!("{}", capture)), + Segment::Static(_) => None, + }) + .collect::>() +} + +fn parse_path(path: &str) -> Vec { + 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), +} diff --git a/axum-macros/src/uri.rs b/axum-macros/src/uri.rs deleted file mode 100644 index 3ddc8443..00000000 --- a/axum-macros/src/uri.rs +++ /dev/null @@ -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 { - 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 { - let mut route = None::; - - for attr in attrs { - if attr.path.is_ident("uri") { - route = Some(attr.parse_args::()?.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::>() - .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 as ::axum::extract::FromRequest>::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 ::axum::extract::FromRequest for #ident - where - B: Send, - { - type Rejection = #rejection; - - async fn from_request( - req: &mut ::axum::extract::RequestParts, - ) -> ::std::result::Result { - #from_request_body - } - } - } -} diff --git a/examples/hello-world/Cargo.toml b/examples/hello-world/Cargo.toml index 65afe194..10c814c5 100644 --- a/examples/hello-world/Cargo.toml +++ b/examples/hello-world/Cargo.toml @@ -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"] } diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index bc464da9..fc541df2 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -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| async {})) + .route(UsersShow::PATH, get(|_: Path| async {})) + .route(UsersEdit::PATH, get(|_: Path| 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("

Hello, World!

") -} - -#[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);