From b42ccc8edaaf94fc35ce2203473ec887984faea1 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 10 Feb 2022 16:46:42 +0100 Subject: [PATCH] wip --- axum-macros/src/lib.rs | 7 ++ axum-macros/src/route.rs | 158 +++++++++++++++++++++++++++++++ examples/hello-world/Cargo.toml | 3 + examples/hello-world/src/main.rs | 21 +++- 4 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 axum-macros/src/route.rs diff --git a/axum-macros/src/lib.rs b/axum-macros/src/lib.rs index ae826426..a5204224 100644 --- a/axum-macros/src/lib.rs +++ b/axum-macros/src/lib.rs @@ -49,6 +49,7 @@ use syn::parse::Parse; mod debug_handler; mod from_request; +mod route; /// Derive an implementation of [`FromRequest`]. /// @@ -385,6 +386,12 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream { return expand_attr_with(_attr, input, debug_handler::expand); } +/// TODO +#[proc_macro_derive(Route, attributes(route))] +pub fn derive_route(input: TokenStream) -> TokenStream { + expand_with(input, route::expand) +} + fn expand_with(input: TokenStream, f: F) -> TokenStream where F: FnOnce(I) -> syn::Result, diff --git a/axum-macros/src/route.rs b/axum-macros/src/route.rs new file mode 100644 index 00000000..ecdb2dba --- /dev/null +++ b/axum-macros/src/route.rs @@ -0,0 +1,158 @@ +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("route") { + 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 route(&self) -> String { + format!(#format_str, #(#set_placeholders,)*) + } + } + } + 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 route(&self) -> String { + format!(#format_str, #(#set_placeholders,)*) + } + } + } + syn::Fields::Unit => quote! { + #vis fn route(&self) -> &'static str { + #route + } + }, + } +} + +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 36b5dfc6..65afe194 100644 --- a/examples/hello-world/Cargo.toml +++ b/examples/hello-world/Cargo.toml @@ -7,3 +7,6 @@ publish = false [dependencies] axum = { path = "../../axum" } tokio = { version = "1.0", features = ["full"] } + +axum-macros = { path = "../../axum-macros" } +serde = { version = "1.0", features = ["derive"] } diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index 466caceb..571c1226 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -4,7 +4,11 @@ //! cargo run -p example-hello-world //! ``` +#![allow(dead_code)] + use axum::{response::Html, routing::get, Router}; +use axum_macros::Route; +use serde::Deserialize; use std::net::SocketAddr; #[tokio::main] @@ -21,6 +25,21 @@ async fn main() { .unwrap(); } -async fn handler() -> Html<&'static str> { +async fn handler(_: UsersShow) -> Html<&'static str> { Html("

Hello, World!

") } + +// #[derive(Deserialize, Route)] +// #[route("/users")] +// struct UsersIndex; + +#[derive(Deserialize, Route)] +#[route("/users/:id/teams/:team_id")] +struct UsersShow { + id: u32, + team_id: u32, +} + +// #[derive(serde::Deserialize)] +// #[route("/users/:id/edit")] +// struct UsersEdit(u32);