From 911c4a788e35e74a6c210b82e15880b68fbb1efa Mon Sep 17 00:00:00 2001
From: David Pedersen <david.pdrsn@gmail.com>
Date: Fri, 28 Jan 2022 10:54:38 +0100
Subject: [PATCH] Support opt-out of extra derived traits for rejections for
 `#[derive(FromRequest)]` (#729)

* Handle structs without fields

* Support opt-out of derived rejection traits

* Handle duplicate opt outs

* Improve error if opting out of `Display` or `Debug` but not `Error`

* document `rejection_derive`

* Handle using both `via` and `rejection_derive`

* don't derive debug for `RejectionDeriveOptOuts`

* Update axum-macros/src/from_request.rs

Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>

Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
---
 axum-macros/src/from_request.rs               | 138 ++++------
 axum-macros/src/from_request/attr.rs          | 243 ++++++++++++++++++
 axum-macros/src/lib.rs                        |  35 +++
 ...opt_out_debug_and_display_without_error.rs |   9 +
 ...out_debug_and_display_without_error.stderr |   5 +
 .../derive_opt_out_debug_without_error.rs     |   9 +
 .../derive_opt_out_debug_without_error.stderr |   5 +
 .../derive_opt_out_display_without_error.rs   |   9 +
 ...erive_opt_out_display_without_error.stderr |   5 +
 .../fail/derive_opt_out_duplicate.rs          |   9 +
 .../fail/derive_opt_out_duplicate.stderr      |   5 +
 .../fail/rejection_derive_and_via.rs          |  10 +
 .../fail/rejection_derive_and_via.stderr      |  13 +
 .../fail/unknown_attr_container.rs            |   7 +
 .../fail/unknown_attr_container.stderr        |   5 +
 ...{unknown_attr.rs => unknown_attr_field.rs} |   0
 ..._attr.stderr => unknown_attr_field.stderr} |   2 +-
 .../fail/via_and_rejection_derive.rs          |  10 +
 .../fail/via_and_rejection_derive.stderr      |  13 +
 .../tests/from_request/pass/derive_opt_out.rs |  37 +++
 .../tests/from_request/pass/empty_named.rs    |  12 +
 .../tests/from_request/pass/empty_tuple.rs    |  12 +
 axum-macros/tests/from_request/pass/unit.rs   |   2 +-
 23 files changed, 504 insertions(+), 91 deletions(-)
 create mode 100644 axum-macros/src/from_request/attr.rs
 create mode 100644 axum-macros/tests/from_request/fail/derive_opt_out_debug_and_display_without_error.rs
 create mode 100644 axum-macros/tests/from_request/fail/derive_opt_out_debug_and_display_without_error.stderr
 create mode 100644 axum-macros/tests/from_request/fail/derive_opt_out_debug_without_error.rs
 create mode 100644 axum-macros/tests/from_request/fail/derive_opt_out_debug_without_error.stderr
 create mode 100644 axum-macros/tests/from_request/fail/derive_opt_out_display_without_error.rs
 create mode 100644 axum-macros/tests/from_request/fail/derive_opt_out_display_without_error.stderr
 create mode 100644 axum-macros/tests/from_request/fail/derive_opt_out_duplicate.rs
 create mode 100644 axum-macros/tests/from_request/fail/derive_opt_out_duplicate.stderr
 create mode 100644 axum-macros/tests/from_request/fail/rejection_derive_and_via.rs
 create mode 100644 axum-macros/tests/from_request/fail/rejection_derive_and_via.stderr
 create mode 100644 axum-macros/tests/from_request/fail/unknown_attr_container.rs
 create mode 100644 axum-macros/tests/from_request/fail/unknown_attr_container.stderr
 rename axum-macros/tests/from_request/fail/{unknown_attr.rs => unknown_attr_field.rs} (100%)
 rename axum-macros/tests/from_request/fail/{unknown_attr.stderr => unknown_attr_field.stderr} (67%)
 create mode 100644 axum-macros/tests/from_request/fail/via_and_rejection_derive.rs
 create mode 100644 axum-macros/tests/from_request/fail/via_and_rejection_derive.stderr
 create mode 100644 axum-macros/tests/from_request/pass/derive_opt_out.rs
 create mode 100644 axum-macros/tests/from_request/pass/empty_named.rs
 create mode 100644 axum-macros/tests/from_request/pass/empty_tuple.rs

diff --git a/axum-macros/src/from_request.rs b/axum-macros/src/from_request.rs
index 8e4fa1e8..158719d3 100644
--- a/axum-macros/src/from_request.rs
+++ b/axum-macros/src/from_request.rs
@@ -1,12 +1,13 @@
+use self::attr::{
+    parse_container_attrs, parse_field_attrs, FromRequestContainerAttr, FromRequestFieldAttr,
+    RejectionDeriveOptOuts,
+};
 use heck::ToUpperCamelCase;
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote, quote_spanned};
-use syn::{
-    parse::{Parse, ParseStream},
-    punctuated::Punctuated,
-    spanned::Spanned,
-    Token,
-};
+use syn::{punctuated::Punctuated, spanned::Spanned, Token};
+
+mod attr;
 
 const GENERICS_ERROR: &str = "`#[derive(FromRequest)] doesn't support generics";
 
@@ -29,12 +30,18 @@ pub(crate) fn expand(item: syn::ItemStruct) -> syn::Result<TokenStream> {
         return Err(syn::Error::new_spanned(where_clause, GENERICS_ERROR));
     }
 
-    let FromRequestAttrs { via } = parse_attrs(&attrs)?;
+    let FromRequestContainerAttr {
+        via,
+        rejection_derive,
+    } = parse_container_attrs(&attrs)?;
 
     if let Some((_, path)) = via {
         impl_by_extracting_all_at_once(ident, fields, path)
     } else {
-        impl_by_extracting_each_field(ident, fields, vis)
+        let rejection_derive_opt_outs = rejection_derive
+            .map(|(_, opt_outs)| opt_outs)
+            .unwrap_or_default();
+        impl_by_extracting_each_field(ident, fields, vis, rejection_derive_opt_outs)
     }
 }
 
@@ -42,15 +49,17 @@ fn impl_by_extracting_each_field(
     ident: syn::Ident,
     fields: syn::Fields,
     vis: syn::Visibility,
+    rejection_derive_opt_outs: RejectionDeriveOptOuts,
 ) -> syn::Result<TokenStream> {
     let extract_fields = extract_fields(&fields)?;
 
-    let (rejection_ident, rejection) = if let syn::Fields::Unit = &fields {
-        (syn::parse_quote!(::std::convert::Infallible), quote! {})
+    let (rejection_ident, rejection) = if has_no_fields(&fields) {
+        (syn::parse_quote!(::std::convert::Infallible), None)
     } else {
         let rejection_ident = rejection_ident(&ident);
-        let rejection = extract_each_field_rejection(&ident, &fields, &vis)?;
-        (rejection_ident, rejection)
+        let rejection =
+            extract_each_field_rejection(&ident, &fields, &vis, rejection_derive_opt_outs)?;
+        (rejection_ident, Some(rejection))
     };
 
     Ok(quote! {
@@ -77,6 +86,14 @@ fn impl_by_extracting_each_field(
     })
 }
 
+fn has_no_fields(fields: &syn::Fields) -> bool {
+    match fields {
+        syn::Fields::Named(fields) => fields.named.is_empty(),
+        syn::Fields::Unnamed(fields) => fields.unnamed.is_empty(),
+        syn::Fields::Unit => true,
+    }
+}
+
 fn rejection_ident(ident: &syn::Ident) -> syn::Type {
     let ident = format_ident!("{}Rejection", ident);
     syn::parse_quote!(#ident)
@@ -87,7 +104,7 @@ fn extract_fields(fields: &syn::Fields) -> syn::Result<Vec<TokenStream>> {
         .iter()
         .enumerate()
         .map(|(index, field)| {
-            let FromRequestAttrs { via } = parse_attrs(&field.attrs)?;
+            let FromRequestFieldAttr { via } = parse_field_attrs(&field.attrs)?;
 
             let member = if let Some(ident) = &field.ident {
                 quote! { #ident }
@@ -211,13 +228,14 @@ fn extract_each_field_rejection(
     ident: &syn::Ident,
     fields: &syn::Fields,
     vis: &syn::Visibility,
+    rejection_derive_opt_outs: RejectionDeriveOptOuts,
 ) -> syn::Result<TokenStream> {
     let rejection_ident = rejection_ident(ident);
 
     let variants = fields
         .iter()
         .map(|field| {
-            let FromRequestAttrs { via } = parse_attrs(&field.attrs)?;
+            let FromRequestFieldAttr { via } = parse_field_attrs(&field.attrs)?;
 
             let field_ty = &field.ty;
             let ty_span = field_ty.span();
@@ -270,7 +288,7 @@ fn extract_each_field_rejection(
         }
     };
 
-    let impl_display = {
+    let impl_display = if rejection_derive_opt_outs.derive_display() {
         let arms = fields
             .iter()
             .map(|field| {
@@ -281,7 +299,7 @@ fn extract_each_field_rejection(
             })
             .collect::<syn::Result<Vec<_>>>()?;
 
-        quote! {
+        Some(quote! {
             #[automatically_derived]
             impl ::std::fmt::Display for #rejection_ident {
                 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
@@ -290,10 +308,12 @@ fn extract_each_field_rejection(
                     }
                 }
             }
-        }
+        })
+    } else {
+        None
     };
 
-    let impl_error = {
+    let impl_error = if rejection_derive_opt_outs.derive_error() {
         let arms = fields
             .iter()
             .map(|field| {
@@ -304,7 +324,7 @@ fn extract_each_field_rejection(
             })
             .collect::<syn::Result<Vec<_>>>()?;
 
-        quote! {
+        Some(quote! {
             #[automatically_derived]
             impl ::std::error::Error for #rejection_ident {
                 fn source(&self) -> ::std::option::Option<&(dyn ::std::error::Error + 'static)> {
@@ -313,11 +333,17 @@ fn extract_each_field_rejection(
                     }
                 }
             }
-        }
+        })
+    } else {
+        None
     };
 
+    let impl_debug = rejection_derive_opt_outs.derive_debug().then(|| {
+        quote! { #[derive(Debug)] }
+    });
+
     Ok(quote! {
-        #[derive(Debug)]
+        #impl_debug
         #vis enum #rejection_ident {
             #(#variants)*
         }
@@ -381,7 +407,7 @@ fn rejection_variant_name(field: &syn::Field) -> syn::Result<syn::Ident> {
         let mut out = String::new();
         rejection_variant_name_for_type(&mut out, &field.ty)?;
 
-        let FromRequestAttrs { via } = parse_attrs(&field.attrs)?;
+        let FromRequestFieldAttr { via } = parse_field_attrs(&field.attrs)?;
         if let Some((_, path)) = via {
             let via_ident = &path.segments.last().unwrap().ident;
             Ok(format_ident!("{}{}", via_ident, out))
@@ -403,7 +429,7 @@ fn impl_by_extracting_all_at_once(
     };
 
     for field in fields {
-        let FromRequestAttrs { via } = parse_attrs(&field.attrs)?;
+        let FromRequestFieldAttr { via } = parse_field_attrs(&field.attrs)?;
         if let Some((via, _)) = via {
             return Err(syn::Error::new_spanned(
                 via,
@@ -437,72 +463,6 @@ fn impl_by_extracting_all_at_once(
     })
 }
 
-#[derive(Default)]
-struct FromRequestAttrs {
-    via: Option<(kw::via, syn::Path)>,
-}
-
-mod kw {
-    syn::custom_keyword!(via);
-}
-
-fn parse_attrs(attrs: &[syn::Attribute]) -> syn::Result<FromRequestAttrs> {
-    enum Attr {
-        FromRequest(Punctuated<FromRequestAttr, Token![,]>),
-    }
-
-    enum FromRequestAttr {
-        Via { via: kw::via, path: syn::Path },
-    }
-
-    impl Parse for FromRequestAttr {
-        fn parse(input: ParseStream) -> syn::Result<Self> {
-            let lh = input.lookahead1();
-            if lh.peek(kw::via) {
-                let via = input.parse::<kw::via>()?;
-                let content;
-                syn::parenthesized!(content in input);
-                content.parse().map(|path| Self::Via { via, path })
-            } else {
-                Err(lh.error())
-            }
-        }
-    }
-
-    let attrs = attrs
-        .iter()
-        .filter(|attr| attr.path.is_ident("from_request"))
-        .map(|attr| {
-            attr.parse_args_with(Punctuated::parse_terminated)
-                .map(Attr::FromRequest)
-        })
-        .collect::<syn::Result<Vec<_>>>()?;
-
-    let mut out = FromRequestAttrs::default();
-    for attr in attrs {
-        match attr {
-            Attr::FromRequest(from_request_attrs) => {
-                for from_request_attr in from_request_attrs {
-                    match from_request_attr {
-                        FromRequestAttr::Via { via, path } => {
-                            if out.via.is_some() {
-                                return Err(syn::Error::new_spanned(
-                                    via,
-                                    "`via` specified more than once",
-                                ));
-                            } else {
-                                out.via = Some((via, path));
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    Ok(out)
-}
-
 #[test]
 fn ui() {
     #[rustversion::stable]
diff --git a/axum-macros/src/from_request/attr.rs b/axum-macros/src/from_request/attr.rs
new file mode 100644
index 00000000..35b63d79
--- /dev/null
+++ b/axum-macros/src/from_request/attr.rs
@@ -0,0 +1,243 @@
+use quote::ToTokens;
+use syn::{
+    parse::{Parse, ParseStream},
+    punctuated::Punctuated,
+    Token,
+};
+
+#[derive(Default)]
+pub(crate) struct FromRequestFieldAttr {
+    pub(crate) via: Option<(kw::via, syn::Path)>,
+}
+
+#[derive(Default)]
+pub(crate) struct FromRequestContainerAttr {
+    pub(crate) via: Option<(kw::via, syn::Path)>,
+    pub(crate) rejection_derive: Option<(kw::rejection_derive, RejectionDeriveOptOuts)>,
+}
+
+pub(crate) mod kw {
+    syn::custom_keyword!(via);
+    syn::custom_keyword!(rejection_derive);
+    syn::custom_keyword!(Display);
+    syn::custom_keyword!(Debug);
+    syn::custom_keyword!(Error);
+}
+
+pub(crate) fn parse_field_attrs(attrs: &[syn::Attribute]) -> syn::Result<FromRequestFieldAttr> {
+    let attrs = parse_attrs(attrs)?;
+
+    let mut out = FromRequestFieldAttr::default();
+
+    for from_request_attr in attrs {
+        match from_request_attr {
+            FieldAttr::Via { via, path } => {
+                if out.via.is_some() {
+                    return Err(double_attr_error("via", via));
+                } else {
+                    out.via = Some((via, path));
+                }
+            }
+        }
+    }
+
+    Ok(out)
+}
+
+pub(crate) fn parse_container_attrs(
+    attrs: &[syn::Attribute],
+) -> syn::Result<FromRequestContainerAttr> {
+    let attrs = parse_attrs(attrs)?;
+
+    let mut out = FromRequestContainerAttr::default();
+
+    for from_request_attr in attrs {
+        match from_request_attr {
+            ContainerAttr::Via { via, path } => {
+                if out.rejection_derive.is_some() {
+                    return Err(syn::Error::new_spanned(
+                        via,
+                        "cannot use both `rejection_derive` and `via`",
+                    ));
+                }
+
+                if out.via.is_some() {
+                    return Err(double_attr_error("via", via));
+                } else {
+                    out.via = Some((via, path));
+                }
+            }
+            ContainerAttr::RejectionDerive {
+                rejection_derive,
+                opt_outs,
+            } => {
+                if out.via.is_some() {
+                    return Err(syn::Error::new_spanned(
+                        rejection_derive,
+                        "cannot use both `via` and `rejection_derive`",
+                    ));
+                }
+
+                if out.rejection_derive.is_some() {
+                    return Err(double_attr_error("rejection_derive", rejection_derive));
+                } else {
+                    out.rejection_derive = Some((rejection_derive, opt_outs));
+                }
+            }
+        }
+    }
+
+    Ok(out)
+}
+
+pub(crate) fn parse_attrs<T>(attrs: &[syn::Attribute]) -> syn::Result<Punctuated<T, Token![,]>>
+where
+    T: Parse,
+{
+    let attrs = attrs
+        .iter()
+        .filter(|attr| attr.path.is_ident("from_request"))
+        .map(|attr| attr.parse_args_with(Punctuated::<T, Token![,]>::parse_terminated))
+        .collect::<syn::Result<Vec<_>>>()?
+        .into_iter()
+        .flatten()
+        .collect::<Punctuated<T, Token![,]>>();
+    Ok(attrs)
+}
+
+fn double_attr_error<T>(ident: &str, spanned: T) -> syn::Error
+where
+    T: ToTokens,
+{
+    syn::Error::new_spanned(spanned, format!("`{}` specified more than once", ident))
+}
+
+enum ContainerAttr {
+    Via {
+        via: kw::via,
+        path: syn::Path,
+    },
+    RejectionDerive {
+        rejection_derive: kw::rejection_derive,
+        opt_outs: RejectionDeriveOptOuts,
+    },
+}
+
+impl Parse for ContainerAttr {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let lh = input.lookahead1();
+        if lh.peek(kw::via) {
+            let via = input.parse::<kw::via>()?;
+            let content;
+            syn::parenthesized!(content in input);
+            content.parse().map(|path| Self::Via { via, path })
+        } else if lh.peek(kw::rejection_derive) {
+            let rejection_derive = input.parse::<kw::rejection_derive>()?;
+            let content;
+            syn::parenthesized!(content in input);
+            content.parse().map(|opt_outs| Self::RejectionDerive {
+                rejection_derive,
+                opt_outs,
+            })
+        } else {
+            Err(lh.error())
+        }
+    }
+}
+
+enum FieldAttr {
+    Via { via: kw::via, path: syn::Path },
+}
+
+impl Parse for FieldAttr {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        let lh = input.lookahead1();
+        if lh.peek(kw::via) {
+            let via = input.parse::<kw::via>()?;
+            let content;
+            syn::parenthesized!(content in input);
+            content.parse().map(|path| Self::Via { via, path })
+        } else {
+            Err(lh.error())
+        }
+    }
+}
+
+#[derive(Default)]
+pub(crate) struct RejectionDeriveOptOuts {
+    debug: Option<kw::Debug>,
+    display: Option<kw::Display>,
+    error: Option<kw::Error>,
+}
+
+impl RejectionDeriveOptOuts {
+    pub(crate) fn derive_debug(&self) -> bool {
+        self.debug.is_none()
+    }
+
+    pub(crate) fn derive_display(&self) -> bool {
+        self.display.is_none()
+    }
+
+    pub(crate) fn derive_error(&self) -> bool {
+        self.error.is_none()
+    }
+}
+
+impl Parse for RejectionDeriveOptOuts {
+    fn parse(input: ParseStream) -> syn::Result<Self> {
+        fn parse_opt_out<T>(out: &mut Option<T>, ident: &str, input: ParseStream) -> syn::Result<()>
+        where
+            T: Parse,
+        {
+            if out.is_some() {
+                Err(input.error(format!("`{}` opt out specified more than once", ident)))
+            } else {
+                *out = Some(input.parse()?);
+                Ok(())
+            }
+        }
+
+        let mut debug = None::<kw::Debug>;
+        let mut display = None::<kw::Display>;
+        let mut error = None::<kw::Error>;
+
+        while !input.is_empty() {
+            input.parse::<Token![!]>()?;
+
+            let lh = input.lookahead1();
+            if lh.peek(kw::Debug) {
+                parse_opt_out(&mut debug, "Debug", input)?;
+            } else if lh.peek(kw::Display) {
+                parse_opt_out(&mut display, "Display", input)?;
+            } else if lh.peek(kw::Error) {
+                parse_opt_out(&mut error, "Error", input)?;
+            } else {
+                return Err(lh.error());
+            }
+
+            input.parse::<Token![,]>().ok();
+        }
+
+        if error.is_none() {
+            match (debug, display) {
+                (Some(debug), Some(_)) => {
+                    return Err(syn::Error::new_spanned(debug, "opt out of `Debug` and `Display` requires also opting out of `Error`. Use `#[from_request(rejection_derive(!Debug, !Display, !Error))]`"));
+                }
+                (Some(debug), None) => {
+                    return Err(syn::Error::new_spanned(debug, "opt out of `Debug` requires also opting out of `Error`. Use `#[from_request(rejection_derive(!Debug, !Error))]`"));
+                }
+                (None, Some(display)) => {
+                    return Err(syn::Error::new_spanned(display, "opt out of `Display` requires also opting out of `Error`. Use `#[from_request(rejection_derive(!Display, !Error))]`"));
+                }
+                (None, None) => {}
+            }
+        }
+
+        Ok(Self {
+            debug,
+            display,
+            error,
+        })
+    }
+}
diff --git a/axum-macros/src/lib.rs b/axum-macros/src/lib.rs
index 0b39951e..ae826426 100644
--- a/axum-macros/src/lib.rs
+++ b/axum-macros/src/lib.rs
@@ -201,6 +201,41 @@ mod from_request;
 /// means the inner rejection types must themselves implement `std::error::Error`. All extractors
 /// in axum does this.
 ///
+/// You can opt out of this using `#[from_request(rejection_derive(...))]`:
+///
+/// ```
+/// use axum_macros::FromRequest;
+/// use axum::{
+///     extract::{FromRequest, RequestParts},
+///     http::StatusCode,
+///     headers::ContentType,
+///     body::Bytes,
+///     async_trait,
+/// };
+///
+/// #[derive(FromRequest)]
+/// #[from_request(rejection_derive(!Display, !Error))]
+/// struct MyExtractor {
+///     other: OtherExtractor,
+/// }
+///
+/// struct OtherExtractor;
+///
+/// #[async_trait]
+/// impl<B> FromRequest<B> for OtherExtractor
+/// where
+///     B: Send + 'static,
+/// {
+///     // this rejection doesn't implement `Display` and `Error`
+///     type Rejection = (StatusCode, String);
+///
+///     async fn from_request(_req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
+///         // ...
+///         # unimplemented!()
+///     }
+/// }
+/// ```
+///
 /// # The whole type at once
 ///
 /// By using `#[from_request(via(...))]` on the container you can extract the whole type at once,
diff --git a/axum-macros/tests/from_request/fail/derive_opt_out_debug_and_display_without_error.rs b/axum-macros/tests/from_request/fail/derive_opt_out_debug_and_display_without_error.rs
new file mode 100644
index 00000000..a8cb0818
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/derive_opt_out_debug_and_display_without_error.rs
@@ -0,0 +1,9 @@
+use axum_macros::FromRequest;
+
+#[derive(FromRequest)]
+#[from_request(rejection_derive(!Debug, !Display))]
+struct Extractor {
+    body: String,
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/fail/derive_opt_out_debug_and_display_without_error.stderr b/axum-macros/tests/from_request/fail/derive_opt_out_debug_and_display_without_error.stderr
new file mode 100644
index 00000000..656c8a54
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/derive_opt_out_debug_and_display_without_error.stderr
@@ -0,0 +1,5 @@
+error: opt out of `Debug` and `Display` requires also opting out of `Error`. Use `#[from_request(rejection_derive(!Debug, !Display, !Error))]`
+ --> tests/from_request/fail/derive_opt_out_debug_and_display_without_error.rs:4:34
+  |
+4 | #[from_request(rejection_derive(!Debug, !Display))]
+  |                                  ^^^^^
diff --git a/axum-macros/tests/from_request/fail/derive_opt_out_debug_without_error.rs b/axum-macros/tests/from_request/fail/derive_opt_out_debug_without_error.rs
new file mode 100644
index 00000000..dbc0aed8
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/derive_opt_out_debug_without_error.rs
@@ -0,0 +1,9 @@
+use axum_macros::FromRequest;
+
+#[derive(FromRequest)]
+#[from_request(rejection_derive(!Debug))]
+struct Extractor {
+    body: String,
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/fail/derive_opt_out_debug_without_error.stderr b/axum-macros/tests/from_request/fail/derive_opt_out_debug_without_error.stderr
new file mode 100644
index 00000000..1d8c2875
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/derive_opt_out_debug_without_error.stderr
@@ -0,0 +1,5 @@
+error: opt out of `Debug` requires also opting out of `Error`. Use `#[from_request(rejection_derive(!Debug, !Error))]`
+ --> tests/from_request/fail/derive_opt_out_debug_without_error.rs:4:34
+  |
+4 | #[from_request(rejection_derive(!Debug))]
+  |                                  ^^^^^
diff --git a/axum-macros/tests/from_request/fail/derive_opt_out_display_without_error.rs b/axum-macros/tests/from_request/fail/derive_opt_out_display_without_error.rs
new file mode 100644
index 00000000..2f478768
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/derive_opt_out_display_without_error.rs
@@ -0,0 +1,9 @@
+use axum_macros::FromRequest;
+
+#[derive(FromRequest)]
+#[from_request(rejection_derive(!Display))]
+struct Extractor {
+    body: String,
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/fail/derive_opt_out_display_without_error.stderr b/axum-macros/tests/from_request/fail/derive_opt_out_display_without_error.stderr
new file mode 100644
index 00000000..0db03759
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/derive_opt_out_display_without_error.stderr
@@ -0,0 +1,5 @@
+error: opt out of `Display` requires also opting out of `Error`. Use `#[from_request(rejection_derive(!Display, !Error))]`
+ --> tests/from_request/fail/derive_opt_out_display_without_error.rs:4:34
+  |
+4 | #[from_request(rejection_derive(!Display))]
+  |                                  ^^^^^^^
diff --git a/axum-macros/tests/from_request/fail/derive_opt_out_duplicate.rs b/axum-macros/tests/from_request/fail/derive_opt_out_duplicate.rs
new file mode 100644
index 00000000..0a0b2eb8
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/derive_opt_out_duplicate.rs
@@ -0,0 +1,9 @@
+use axum_macros::FromRequest;
+
+#[derive(FromRequest)]
+#[from_request(rejection_derive(!Error, !Error))]
+struct Extractor {
+    body: String,
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/fail/derive_opt_out_duplicate.stderr b/axum-macros/tests/from_request/fail/derive_opt_out_duplicate.stderr
new file mode 100644
index 00000000..7ae523d4
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/derive_opt_out_duplicate.stderr
@@ -0,0 +1,5 @@
+error: `Error` opt out specified more than once
+ --> tests/from_request/fail/derive_opt_out_duplicate.rs:4:42
+  |
+4 | #[from_request(rejection_derive(!Error, !Error))]
+  |                                          ^^^^^
diff --git a/axum-macros/tests/from_request/fail/rejection_derive_and_via.rs b/axum-macros/tests/from_request/fail/rejection_derive_and_via.rs
new file mode 100644
index 00000000..bb658f11
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/rejection_derive_and_via.rs
@@ -0,0 +1,10 @@
+use axum_macros::FromRequest;
+use axum::extract::Extension;
+
+#[derive(FromRequest, Clone)]
+#[from_request(rejection_derive(!Error), via(Extension))]
+struct Extractor {
+    config: String,
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/fail/rejection_derive_and_via.stderr b/axum-macros/tests/from_request/fail/rejection_derive_and_via.stderr
new file mode 100644
index 00000000..59c6b0be
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/rejection_derive_and_via.stderr
@@ -0,0 +1,13 @@
+error: cannot use both `rejection_derive` and `via`
+ --> tests/from_request/fail/rejection_derive_and_via.rs:5:42
+  |
+5 | #[from_request(rejection_derive(!Error), via(Extension))]
+  |                                          ^^^
+
+warning: unused import: `axum::extract::Extension`
+ --> tests/from_request/fail/rejection_derive_and_via.rs:2:5
+  |
+2 | use axum::extract::Extension;
+  |     ^^^^^^^^^^^^^^^^^^^^^^^^
+  |
+  = note: `#[warn(unused_imports)]` on by default
diff --git a/axum-macros/tests/from_request/fail/unknown_attr_container.rs b/axum-macros/tests/from_request/fail/unknown_attr_container.rs
new file mode 100644
index 00000000..f6d06e02
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/unknown_attr_container.rs
@@ -0,0 +1,7 @@
+use axum_macros::FromRequest;
+
+#[derive(FromRequest)]
+#[from_request(foo)]
+struct Extractor;
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/fail/unknown_attr_container.stderr b/axum-macros/tests/from_request/fail/unknown_attr_container.stderr
new file mode 100644
index 00000000..10608bb6
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/unknown_attr_container.stderr
@@ -0,0 +1,5 @@
+error: expected `via` or `rejection_derive`
+ --> tests/from_request/fail/unknown_attr_container.rs:4:16
+  |
+4 | #[from_request(foo)]
+  |                ^^^
diff --git a/axum-macros/tests/from_request/fail/unknown_attr.rs b/axum-macros/tests/from_request/fail/unknown_attr_field.rs
similarity index 100%
rename from axum-macros/tests/from_request/fail/unknown_attr.rs
rename to axum-macros/tests/from_request/fail/unknown_attr_field.rs
diff --git a/axum-macros/tests/from_request/fail/unknown_attr.stderr b/axum-macros/tests/from_request/fail/unknown_attr_field.stderr
similarity index 67%
rename from axum-macros/tests/from_request/fail/unknown_attr.stderr
rename to axum-macros/tests/from_request/fail/unknown_attr_field.stderr
index b6422fed..9b626a94 100644
--- a/axum-macros/tests/from_request/fail/unknown_attr.stderr
+++ b/axum-macros/tests/from_request/fail/unknown_attr_field.stderr
@@ -1,5 +1,5 @@
 error: expected `via`
- --> tests/from_request/fail/unknown_attr.rs:4:33
+ --> tests/from_request/fail/unknown_attr_field.rs:4:33
   |
 4 | struct Extractor(#[from_request(foo)] String);
   |                                 ^^^
diff --git a/axum-macros/tests/from_request/fail/via_and_rejection_derive.rs b/axum-macros/tests/from_request/fail/via_and_rejection_derive.rs
new file mode 100644
index 00000000..8c183a60
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/via_and_rejection_derive.rs
@@ -0,0 +1,10 @@
+use axum_macros::FromRequest;
+use axum::extract::Extension;
+
+#[derive(FromRequest, Clone)]
+#[from_request(via(Extension), rejection_derive(!Error))]
+struct Extractor {
+    config: String,
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/fail/via_and_rejection_derive.stderr b/axum-macros/tests/from_request/fail/via_and_rejection_derive.stderr
new file mode 100644
index 00000000..25f2011b
--- /dev/null
+++ b/axum-macros/tests/from_request/fail/via_and_rejection_derive.stderr
@@ -0,0 +1,13 @@
+error: cannot use both `via` and `rejection_derive`
+ --> tests/from_request/fail/via_and_rejection_derive.rs:5:32
+  |
+5 | #[from_request(via(Extension), rejection_derive(!Error))]
+  |                                ^^^^^^^^^^^^^^^^
+
+warning: unused import: `axum::extract::Extension`
+ --> tests/from_request/fail/via_and_rejection_derive.rs:2:5
+  |
+2 | use axum::extract::Extension;
+  |     ^^^^^^^^^^^^^^^^^^^^^^^^
+  |
+  = note: `#[warn(unused_imports)]` on by default
diff --git a/axum-macros/tests/from_request/pass/derive_opt_out.rs b/axum-macros/tests/from_request/pass/derive_opt_out.rs
new file mode 100644
index 00000000..0bf24c73
--- /dev/null
+++ b/axum-macros/tests/from_request/pass/derive_opt_out.rs
@@ -0,0 +1,37 @@
+use axum::{
+    async_trait,
+    extract::{FromRequest, RequestParts},
+    response::{IntoResponse, Response},
+};
+use axum_macros::FromRequest;
+
+#[derive(FromRequest)]
+#[from_request(rejection_derive(!Display, !Error))]
+struct Extractor {
+    other: OtherExtractor,
+}
+
+struct OtherExtractor;
+
+#[async_trait]
+impl<B> FromRequest<B> for OtherExtractor
+where
+    B: Send + 'static,
+{
+    type Rejection = OtherExtractorRejection;
+
+    async fn from_request(_req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
+        unimplemented!()
+    }
+}
+
+#[derive(Debug)]
+struct OtherExtractorRejection;
+
+impl IntoResponse for OtherExtractorRejection {
+    fn into_response(self) -> Response {
+        unimplemented!()
+    }
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/pass/empty_named.rs b/axum-macros/tests/from_request/pass/empty_named.rs
new file mode 100644
index 00000000..2cc5dda8
--- /dev/null
+++ b/axum-macros/tests/from_request/pass/empty_named.rs
@@ -0,0 +1,12 @@
+use axum_macros::FromRequest;
+
+#[derive(FromRequest)]
+struct Extractor {}
+
+fn assert_from_request()
+where
+    Extractor: axum::extract::FromRequest<axum::body::Body, Rejection = std::convert::Infallible>,
+{
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/pass/empty_tuple.rs b/axum-macros/tests/from_request/pass/empty_tuple.rs
new file mode 100644
index 00000000..bbb525fa
--- /dev/null
+++ b/axum-macros/tests/from_request/pass/empty_tuple.rs
@@ -0,0 +1,12 @@
+use axum_macros::FromRequest;
+
+#[derive(FromRequest)]
+struct Extractor();
+
+fn assert_from_request()
+where
+    Extractor: axum::extract::FromRequest<axum::body::Body, Rejection = std::convert::Infallible>,
+{
+}
+
+fn main() {}
diff --git a/axum-macros/tests/from_request/pass/unit.rs b/axum-macros/tests/from_request/pass/unit.rs
index bbf58c3c..57f774d1 100644
--- a/axum-macros/tests/from_request/pass/unit.rs
+++ b/axum-macros/tests/from_request/pass/unit.rs
@@ -5,7 +5,7 @@ struct Extractor;
 
 fn assert_from_request()
 where
-    Extractor: axum::extract::FromRequest<axum::body::Body>,
+    Extractor: axum::extract::FromRequest<axum::body::Body, Rejection = std::convert::Infallible>,
 {
 }