From 3ca5d87f455ddc94a66207f79e2e5410112300c4 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Wed, 28 Sep 2022 22:40:27 +0200 Subject: [PATCH 01/13] add `#[derive(FromRef)]` --- axum-macros/src/from_ref.rs | 95 ++++++++++++++++++++++++++++++++ axum-macros/src/lib.rs | 6 ++ examples/hello-world/Cargo.toml | 1 + examples/hello-world/src/main.rs | 18 +++--- 4 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 axum-macros/src/from_ref.rs diff --git a/axum-macros/src/from_ref.rs b/axum-macros/src/from_ref.rs new file mode 100644 index 0000000000..e4dc421e18 --- /dev/null +++ b/axum-macros/src/from_ref.rs @@ -0,0 +1,95 @@ +use crate::attr_parsing::{parse_attrs, Combine}; +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, quote_spanned}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, + Field, ItemStruct, Token, +}; + +pub(crate) fn expand(item: ItemStruct) -> syn::Result { + let from_ref_impls = item + .fields + .iter() + .enumerate() + .map(|(idx, field)| expand_field(&item.ident, idx, field)) + .map(|result| match result { + Ok(tokens) => tokens, + Err(err) => err.into_compile_error(), + }); + + Ok(quote! { + #(#from_ref_impls)* + }) +} + +fn expand_field(state: &Ident, idx: usize, field: &Field) -> syn::Result { + let FieldAttrs { skip } = parse_attrs::("from_ref", &field.attrs)?; + + if skip.is_some() { + return Ok(quote! {}); + } + + let field_ty = &field.ty; + let span = field.ty.span(); + + let body = if let Some(field_ident) = &field.ident { + quote_spanned! {span=> state.#field_ident.clone() } + } else { + let idx = syn::Index { + index: idx as _, + span: field.span(), + }; + quote_spanned! {span=> state.#idx.clone() } + }; + + Ok(quote_spanned! {span=> + impl ::axum::extract::FromRef<#state> for #field_ty { + fn from_ref(state: &#state) -> Self { + #body + } + } + }) +} + +pub(crate) mod kw { + syn::custom_keyword!(skip); +} + +#[derive(Default)] +pub(super) struct FieldAttrs { + pub(super) skip: Option, +} + +impl Parse for FieldAttrs { + fn parse(input: ParseStream) -> syn::Result { + let mut skip = None; + + while !input.is_empty() { + let lh = input.lookahead1(); + if lh.peek(kw::skip) { + skip = Some(input.parse()?); + } else { + return Err(lh.error()); + } + + let _ = input.parse::(); + } + + Ok(Self { skip }) + } +} + +impl Combine for FieldAttrs { + fn combine(mut self, other: Self) -> syn::Result { + let Self { skip } = other; + if let Some(kw) = skip { + if self.skip.is_some() { + let msg = "`skip` specified more than once"; + return Err(syn::Error::new_spanned(kw, msg)); + } + self.skip = Some(kw); + } + Ok(self) + } +} diff --git a/axum-macros/src/lib.rs b/axum-macros/src/lib.rs index 0529315c31..6dcbb4ac59 100644 --- a/axum-macros/src/lib.rs +++ b/axum-macros/src/lib.rs @@ -51,6 +51,7 @@ use syn::{parse::Parse, Type}; mod attr_parsing; mod debug_handler; +mod from_ref; mod from_request; mod typed_path; mod with_position; @@ -575,6 +576,11 @@ pub fn derive_typed_path(input: TokenStream) -> TokenStream { expand_with(input, typed_path::expand) } +#[proc_macro_derive(FromRef, attributes(from_ref))] +pub fn derive_from_ref(item: TokenStream) -> TokenStream { + expand_with(item, |item| from_ref::expand(item)) +} + fn expand_with(input: TokenStream, f: F) -> TokenStream where F: FnOnce(I) -> syn::Result, diff --git a/examples/hello-world/Cargo.toml b/examples/hello-world/Cargo.toml index 6fbee18a76..8a8e96818f 100644 --- a/examples/hello-world/Cargo.toml +++ b/examples/hello-world/Cargo.toml @@ -6,4 +6,5 @@ publish = false [dependencies] axum = { path = "../../axum" } +axum-macros = { path = "../../axum-macros" } tokio = { version = "1.0", features = ["full"] } diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index ed115f6b2f..6f48910d35 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -4,23 +4,27 @@ //! cd examples && cargo run -p example-hello-world //! ``` -use axum::{response::Html, routing::get, Router}; +use axum::{extract::State, routing::get, Router}; +use axum_macros::FromRef; 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::with_state(AppState::default()).route("/", get(|_: State| async {})); - // run it let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); - println!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } -async fn handler() -> Html<&'static str> { - Html("

Hello, World!

") +#[derive(FromRef, Default)] +struct AppState { + token: String, + #[from_ref(skip)] + skip: NotClone, } + +#[derive(Default)] +struct NotClone {} From 29fd8aafd9630fd39eb5a1b39c418d6348db8046 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 29 Sep 2022 17:07:53 +0200 Subject: [PATCH 02/13] tests --- axum-core/src/extract/from_ref.rs | 3 +++ axum-macros/src/lib.rs | 38 ++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/axum-core/src/extract/from_ref.rs b/axum-core/src/extract/from_ref.rs index c0124140e5..cee9d3377a 100644 --- a/axum-core/src/extract/from_ref.rs +++ b/axum-core/src/extract/from_ref.rs @@ -5,7 +5,10 @@ /// /// See [`State`] for more details on how library authors should use this trait. /// +/// This trait can be derived using `#[derive(axum_macros::FromRef)]`. +/// /// [`State`]: https://docs.rs/axum/0.6/axum/extract/struct.State.html +/// [`#[derive(axum_macros::FromRef)]`]: https://docs.rs/axum-macros/latest/axum_macros/derive.FromRef.html // NOTE: This trait is defined in axum-core, even though it is mainly used with `State` which is // defined in axum. That allows crate authors to use it when implementing extractors. pub trait FromRef { diff --git a/axum-macros/src/lib.rs b/axum-macros/src/lib.rs index 6dcbb4ac59..2148bd4151 100644 --- a/axum-macros/src/lib.rs +++ b/axum-macros/src/lib.rs @@ -576,9 +576,45 @@ pub fn derive_typed_path(input: TokenStream) -> TokenStream { expand_with(input, typed_path::expand) } +/// Derive an implementation of [`FromRef`] for each field in a struct. +/// +/// # Example +/// +/// ``` +/// use axum_macros::FromRef; +/// use axum::{Router, routing::get, extract::State}; +/// +/// # +/// # type AuthToken = String; +/// # type DatabasePool = (); +/// # +/// // This will implement `FromRef` for each field in the struct. +/// #[derive(FromRef)] +/// struct AppState { +/// auth_token: AuthToken, +/// database_pool: DatabasePool, +/// } +/// +/// // So those types can be extracted via `State` +/// async fn handler(State(auth_token): State) {} +/// +/// async fn other_handler(State(database_pool): State) {} +/// +/// # let auth_token = Default::default(); +/// # let database_pool = Default::default(); +/// let state = AppState { +/// auth_token, +/// database_pool, +/// }; +/// +/// let app = Router::with_state(state).route("/", get(handler).post(other_handler)); +/// # let _: Router = app; +/// ``` +/// +/// [`FromRef`]: https://docs.rs/axum/latest/axum/extract/trait.FromRef.html #[proc_macro_derive(FromRef, attributes(from_ref))] pub fn derive_from_ref(item: TokenStream) -> TokenStream { - expand_with(item, |item| from_ref::expand(item)) + expand_with(item, from_ref::expand) } fn expand_with(input: TokenStream, f: F) -> TokenStream From 540f2a7db9a3449ed53ed437c83415753576b5d5 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 29 Sep 2022 17:08:14 +0200 Subject: [PATCH 03/13] don't support skipping fields probably wouldn't work at all since the whole state likely needs `Clone` --- axum-macros/src/from_ref.rs | 53 +------------------------------------ 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/axum-macros/src/from_ref.rs b/axum-macros/src/from_ref.rs index e4dc421e18..8a2acee1e1 100644 --- a/axum-macros/src/from_ref.rs +++ b/axum-macros/src/from_ref.rs @@ -1,11 +1,6 @@ -use crate::attr_parsing::{parse_attrs, Combine}; use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned}; -use syn::{ - parse::{Parse, ParseStream}, - spanned::Spanned, - Field, ItemStruct, Token, -}; +use syn::{spanned::Spanned, Field, ItemStruct}; pub(crate) fn expand(item: ItemStruct) -> syn::Result { let from_ref_impls = item @@ -24,12 +19,6 @@ pub(crate) fn expand(item: ItemStruct) -> syn::Result { } fn expand_field(state: &Ident, idx: usize, field: &Field) -> syn::Result { - let FieldAttrs { skip } = parse_attrs::("from_ref", &field.attrs)?; - - if skip.is_some() { - return Ok(quote! {}); - } - let field_ty = &field.ty; let span = field.ty.span(); @@ -52,44 +41,4 @@ fn expand_field(state: &Ident, idx: usize, field: &Field) -> syn::Result, -} - -impl Parse for FieldAttrs { - fn parse(input: ParseStream) -> syn::Result { - let mut skip = None; - - while !input.is_empty() { - let lh = input.lookahead1(); - if lh.peek(kw::skip) { - skip = Some(input.parse()?); - } else { - return Err(lh.error()); - } - - let _ = input.parse::(); - } - - Ok(Self { skip }) - } -} - -impl Combine for FieldAttrs { - fn combine(mut self, other: Self) -> syn::Result { - let Self { skip } = other; - if let Some(kw) = skip { - if self.skip.is_some() { - let msg = "`skip` specified more than once"; - return Err(syn::Error::new_spanned(kw, msg)); - } - self.skip = Some(kw); - } - Ok(self) - } } From c871de268a9588adab6f3159815603240be8389e Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 29 Sep 2022 17:08:51 +0200 Subject: [PATCH 04/13] UI tests --- axum-macros/src/from_ref.rs | 3 +++ axum-macros/tests/from_ref/pass/basic.rs | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 axum-macros/tests/from_ref/pass/basic.rs diff --git a/axum-macros/src/from_ref.rs b/axum-macros/src/from_ref.rs index 8a2acee1e1..27065085ea 100644 --- a/axum-macros/src/from_ref.rs +++ b/axum-macros/src/from_ref.rs @@ -41,4 +41,7 @@ fn expand_field(state: &Ident, idx: usize, field: &Field) -> syn::Result) {} + +fn main() { + let state = AppState { + auth_token: Default::default(), + }; + + let _: Router = Router::with_state(state).route("/", get(handler)); +} From 37a7f2c711f1530e73cfb059d26c52242214da7f Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 29 Sep 2022 17:08:55 +0200 Subject: [PATCH 05/13] changelog --- axum-macros/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum-macros/CHANGELOG.md b/axum-macros/CHANGELOG.md index 47fa8b1e0d..73db1e4a25 100644 --- a/axum-macros/CHANGELOG.md +++ b/axum-macros/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased -- None +- **added:** Add `#[derive(FromRef)]` # 0.3.0-rc.1 (23. August, 2022) From 9c43baa7c4c29054f6578e99d617c0956088b6d3 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 29 Sep 2022 17:16:54 +0200 Subject: [PATCH 06/13] changelog link --- axum-macros/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/axum-macros/CHANGELOG.md b/axum-macros/CHANGELOG.md index 73db1e4a25..5a5789c7cd 100644 --- a/axum-macros/CHANGELOG.md +++ b/axum-macros/CHANGELOG.md @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased -- **added:** Add `#[derive(FromRef)]` +- **added:** Add `#[derive(FromRef)]` ([#1430]) + +[#1430]: https://github.com/tokio-rs/axum/pull/1430 # 0.3.0-rc.1 (23. August, 2022) From 215349c8c924d741d6e4ad1c5483ba8341db3b13 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 29 Sep 2022 17:19:25 +0200 Subject: [PATCH 07/13] revert hello-world example, used for testing --- examples/hello-world/Cargo.toml | 1 - examples/hello-world/src/main.rs | 18 +++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/examples/hello-world/Cargo.toml b/examples/hello-world/Cargo.toml index 8a8e96818f..6fbee18a76 100644 --- a/examples/hello-world/Cargo.toml +++ b/examples/hello-world/Cargo.toml @@ -6,5 +6,4 @@ publish = false [dependencies] axum = { path = "../../axum" } -axum-macros = { path = "../../axum-macros" } tokio = { version = "1.0", features = ["full"] } diff --git a/examples/hello-world/src/main.rs b/examples/hello-world/src/main.rs index 6f48910d35..ed115f6b2f 100644 --- a/examples/hello-world/src/main.rs +++ b/examples/hello-world/src/main.rs @@ -4,27 +4,23 @@ //! cd examples && cargo run -p example-hello-world //! ``` -use axum::{extract::State, routing::get, Router}; -use axum_macros::FromRef; +use axum::{response::Html, routing::get, Router}; use std::net::SocketAddr; #[tokio::main] async fn main() { - let app = Router::with_state(AppState::default()).route("/", get(|_: State| async {})); + // build our application with a route + let app = Router::new().route("/", get(handler)); + // run it let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + println!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } -#[derive(FromRef, Default)] -struct AppState { - token: String, - #[from_ref(skip)] - skip: NotClone, +async fn handler() -> Html<&'static str> { + Html("

Hello, World!

") } - -#[derive(Default)] -struct NotClone {} From 280f483ab8a38b8403b5cab7186199a98f49222f Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 9 Oct 2022 22:55:03 +0200 Subject: [PATCH 08/13] Re-export `#[derive(FromRef)]` --- axum/src/extract/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum/src/extract/mod.rs b/axum/src/extract/mod.rs index 9dde11f518..a11921df41 100644 --- a/axum/src/extract/mod.rs +++ b/axum/src/extract/mod.rs @@ -19,7 +19,7 @@ mod state; pub use axum_core::extract::{DefaultBodyLimit, FromRef, FromRequest, FromRequestParts}; #[cfg(feature = "macros")] -pub use axum_macros::{FromRequest, FromRequestParts}; +pub use axum_macros::{FromRef, FromRequest, FromRequestParts}; #[doc(inline)] #[allow(deprecated)] From 06f2de85e85bc8fc520e50c998bc7617432b9b52 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 9 Oct 2022 22:56:39 +0200 Subject: [PATCH 09/13] Don't need to return `Result` --- axum-macros/src/from_ref.rs | 18 +++++++----------- axum-macros/src/lib.rs | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/axum-macros/src/from_ref.rs b/axum-macros/src/from_ref.rs index 27065085ea..f15729f86f 100644 --- a/axum-macros/src/from_ref.rs +++ b/axum-macros/src/from_ref.rs @@ -2,23 +2,19 @@ use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned}; use syn::{spanned::Spanned, Field, ItemStruct}; -pub(crate) fn expand(item: ItemStruct) -> syn::Result { +pub(crate) fn expand(item: ItemStruct) -> TokenStream { let from_ref_impls = item .fields .iter() .enumerate() - .map(|(idx, field)| expand_field(&item.ident, idx, field)) - .map(|result| match result { - Ok(tokens) => tokens, - Err(err) => err.into_compile_error(), - }); + .map(|(idx, field)| expand_field(&item.ident, idx, field)); - Ok(quote! { + quote! { #(#from_ref_impls)* - }) + } } -fn expand_field(state: &Ident, idx: usize, field: &Field) -> syn::Result { +fn expand_field(state: &Ident, idx: usize, field: &Field) -> TokenStream { let field_ty = &field.ty; let span = field.ty.span(); @@ -32,13 +28,13 @@ fn expand_field(state: &Ident, idx: usize, field: &Field) -> syn::Result state.#idx.clone() } }; - Ok(quote_spanned! {span=> + quote_spanned! {span=> impl ::axum::extract::FromRef<#state> for #field_ty { fn from_ref(state: &#state) -> Self { #body } } - }) + } } #[test] diff --git a/axum-macros/src/lib.rs b/axum-macros/src/lib.rs index 2148bd4151..772a54deb5 100644 --- a/axum-macros/src/lib.rs +++ b/axum-macros/src/lib.rs @@ -614,7 +614,7 @@ pub fn derive_typed_path(input: TokenStream) -> TokenStream { /// [`FromRef`]: https://docs.rs/axum/latest/axum/extract/trait.FromRef.html #[proc_macro_derive(FromRef, attributes(from_ref))] pub fn derive_from_ref(item: TokenStream) -> TokenStream { - expand_with(item, from_ref::expand) + expand_with(item, |item| Ok(from_ref::expand(item))) } fn expand_with(input: TokenStream, f: F) -> TokenStream From 48dd51a27fb97843a9e7c4b2846ef2d021df4b50 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 9 Oct 2022 22:57:21 +0200 Subject: [PATCH 10/13] use `collect` instead of quoting the iterator --- axum-macros/src/from_ref.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/axum-macros/src/from_ref.rs b/axum-macros/src/from_ref.rs index f15729f86f..3ebb418015 100644 --- a/axum-macros/src/from_ref.rs +++ b/axum-macros/src/from_ref.rs @@ -1,17 +1,13 @@ use proc_macro2::{Ident, TokenStream}; -use quote::{quote, quote_spanned}; +use quote::quote_spanned; use syn::{spanned::Spanned, Field, ItemStruct}; pub(crate) fn expand(item: ItemStruct) -> TokenStream { - let from_ref_impls = item - .fields + item.fields .iter() .enumerate() - .map(|(idx, field)| expand_field(&item.ident, idx, field)); - - quote! { - #(#from_ref_impls)* - } + .map(|(idx, field)| expand_field(&item.ident, idx, field)) + .collect() } fn expand_field(state: &Ident, idx: usize, field: &Field) -> TokenStream { From 52776470015b0602a3310003b29b1d735da07729 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Sun, 9 Oct 2022 22:58:14 +0200 Subject: [PATCH 11/13] Mention it in axum's changelog --- axum/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index 2589459bb8..ec82d88ac5 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 named `HandlerService` ([#1418]) - **added:** String and binary `From` impls have been added to `extract::ws::Message` to be more inline with `tungstenite` ([#1421]) +- **added:** Add `#[derive(axum::extract::FromRef)]` ([#1430]) [#1368]: https://github.com/tokio-rs/axum/pull/1368 [#1371]: https://github.com/tokio-rs/axum/pull/1371 From 1b01d976d6796668744cc07e39905c70140e0c00 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 10 Oct 2022 18:14:40 +0200 Subject: [PATCH 12/13] Fix tests --- axum-macros/tests/from_ref/pass/basic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum-macros/tests/from_ref/pass/basic.rs b/axum-macros/tests/from_ref/pass/basic.rs index 1b22e34fb3..4a6631d308 100644 --- a/axum-macros/tests/from_ref/pass/basic.rs +++ b/axum-macros/tests/from_ref/pass/basic.rs @@ -2,7 +2,7 @@ use axum_macros::FromRef; use axum::{Router, routing::get, extract::State}; // This will implement `FromRef` for each field in the struct. -#[derive(FromRef)] +#[derive(Clone, FromRef)] struct AppState { auth_token: String, } From 33e195ab9c388aaaff881996519285423f8b072d Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Mon, 10 Oct 2022 20:25:37 +0200 Subject: [PATCH 13/13] fix doc tests --- axum-macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/axum-macros/src/lib.rs b/axum-macros/src/lib.rs index f8f04dbfd3..3d864cd5b2 100644 --- a/axum-macros/src/lib.rs +++ b/axum-macros/src/lib.rs @@ -587,7 +587,7 @@ pub fn derive_typed_path(input: TokenStream) -> TokenStream { /// # type DatabasePool = (); /// # /// // This will implement `FromRef` for each field in the struct. -/// #[derive(FromRef)] +/// #[derive(FromRef, Clone)] /// struct AppState { /// auth_token: AuthToken, /// database_pool: DatabasePool,