From ac7037d28208403d6030a47fdd9b0ff9cf2a9009 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Fri, 12 Aug 2022 18:05:27 +0200 Subject: [PATCH] Support using a different rejection for `#[derive(FromRequest)]` (#1256) --- axum-macros/CHANGELOG.md | 3 + axum-macros/Cargo.toml | 1 + axum-macros/src/from_request.rs | 247 +++++++++++++++--- axum-macros/src/from_request/attr.rs | 66 ++++- axum-macros/src/lib.rs | 144 +++++++++- .../tests/from_request/fail/generic.stderr | 6 +- .../from_request/fail/generic_without_via.rs | 11 + .../fail/generic_without_via.stderr | 29 ++ .../fail/generic_without_via_rejection.rs | 12 + .../fail/generic_without_via_rejection.stderr | 29 ++ .../generic_without_via_rejection_derive.rs | 12 + ...eneric_without_via_rejection_derive.stderr | 29 ++ .../override_rejection_on_enum_without_via.rs | 33 +++ ...rride_rejection_on_enum_without_via.stderr | 37 +++ .../fail/unknown_attr_container.stderr | 2 +- .../from_request/pass/override_rejection.rs | 61 +++++ .../override_rejection_with_via_on_enum.rs | 33 +++ .../override_rejection_with_via_on_struct.rs | 40 +++ 18 files changed, 750 insertions(+), 45 deletions(-) create mode 100644 axum-macros/tests/from_request/fail/generic_without_via.rs create mode 100644 axum-macros/tests/from_request/fail/generic_without_via.stderr create mode 100644 axum-macros/tests/from_request/fail/generic_without_via_rejection.rs create mode 100644 axum-macros/tests/from_request/fail/generic_without_via_rejection.stderr create mode 100644 axum-macros/tests/from_request/fail/generic_without_via_rejection_derive.rs create mode 100644 axum-macros/tests/from_request/fail/generic_without_via_rejection_derive.stderr create mode 100644 axum-macros/tests/from_request/fail/override_rejection_on_enum_without_via.rs create mode 100644 axum-macros/tests/from_request/fail/override_rejection_on_enum_without_via.stderr create mode 100644 axum-macros/tests/from_request/pass/override_rejection.rs create mode 100644 axum-macros/tests/from_request/pass/override_rejection_with_via_on_enum.rs create mode 100644 axum-macros/tests/from_request/pass/override_rejection_with_via_on_struct.rs diff --git a/axum-macros/CHANGELOG.md b/axum-macros/CHANGELOG.md index 63597979..1f71e638 100644 --- a/axum-macros/CHANGELOG.md +++ b/axum-macros/CHANGELOG.md @@ -8,8 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # Unreleased - **change:** axum-macro's MSRV is now 1.60 ([#1239]) +- **added:** Support using a different rejection for `#[derive(FromRequest)]` + with `#[from_request(rejection(MyRejection))]` ([#1256]) [#1239]: https://github.com/tokio-rs/axum/pull/1239 +[#1256]: https://github.com/tokio-rs/axum/pull/1256 # 0.2.3 (27. June, 2022) diff --git a/axum-macros/Cargo.toml b/axum-macros/Cargo.toml index b8e0a6e2..50d7aafc 100644 --- a/axum-macros/Cargo.toml +++ b/axum-macros/Cargo.toml @@ -25,6 +25,7 @@ axum = { path = "../axum", version = "0.5", features = ["headers"] } axum-extra = { path = "../axum-extra", version = "0.3", features = ["typed-routing"] } rustversion = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" syn = { version = "1.0", features = ["full", "extra-traits"] } tokio = { version = "1.0", features = ["full"] } trybuild = "1.0.63" diff --git a/axum-macros/src/from_request.rs b/axum-macros/src/from_request.rs index 76634ee9..f017af7f 100644 --- a/axum-macros/src/from_request.rs +++ b/axum-macros/src/from_request.rs @@ -5,7 +5,7 @@ use self::attr::{ use heck::ToUpperCamelCase; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned}; -use syn::{punctuated::Punctuated, spanned::Spanned, Token}; +use syn::{punctuated::Punctuated, spanned::Spanned, Ident, Token}; mod attr; @@ -22,21 +22,45 @@ pub(crate) fn expand(item: syn::Item) -> syn::Result { struct_token: _, } = item; - error_on_generics(generics)?; + let generic_ident = parse_single_generic_type_on_struct(generics, &fields)?; match parse_container_attrs(&attrs)? { - FromRequestContainerAttr::Via(path) => { - impl_struct_by_extracting_all_at_once(ident, fields, path) + FromRequestContainerAttr::Via { path, rejection } => { + impl_struct_by_extracting_all_at_once( + ident, + fields, + path, + rejection, + generic_ident, + ) } FromRequestContainerAttr::RejectionDerive(_, opt_outs) => { - impl_struct_by_extracting_each_field(ident, fields, vis, opt_outs) + error_on_generic_ident(generic_ident)?; + + impl_struct_by_extracting_each_field(ident, fields, vis, opt_outs, None) + } + FromRequestContainerAttr::Rejection(rejection) => { + error_on_generic_ident(generic_ident)?; + + impl_struct_by_extracting_each_field( + ident, + fields, + vis, + RejectionDeriveOptOuts::default(), + Some(rejection), + ) + } + FromRequestContainerAttr::None => { + error_on_generic_ident(generic_ident)?; + + impl_struct_by_extracting_each_field( + ident, + fields, + vis, + RejectionDeriveOptOuts::default(), + None, + ) } - FromRequestContainerAttr::None => impl_struct_by_extracting_each_field( - ident, - fields, - vis, - RejectionDeriveOptOuts::default(), - ), } } syn::Item::Enum(item) => { @@ -50,11 +74,19 @@ pub(crate) fn expand(item: syn::Item) -> syn::Result { variants, } = item; - error_on_generics(generics)?; + const GENERICS_ERROR: &str = "`#[derive(FromRequest)] on enums don't support generics"; + + if !generics.params.is_empty() { + return Err(syn::Error::new_spanned(generics, GENERICS_ERROR)); + } + + if let Some(where_clause) = generics.where_clause { + return Err(syn::Error::new_spanned(where_clause, GENERICS_ERROR)); + } match parse_container_attrs(&attrs)? { - FromRequestContainerAttr::Via(path) => { - impl_enum_by_extracting_all_at_once(ident, variants, path) + FromRequestContainerAttr::Via { path, rejection } => { + impl_enum_by_extracting_all_at_once(ident, variants, path, rejection) } FromRequestContainerAttr::RejectionDerive(rejection_derive, _) => { Err(syn::Error::new_spanned( @@ -62,6 +94,10 @@ pub(crate) fn expand(item: syn::Item) -> syn::Result { "cannot use `rejection_derive` on enums", )) } + FromRequestContainerAttr::Rejection(rejection) => Err(syn::Error::new_spanned( + rejection, + "cannot use `rejection` without `via`", + )), FromRequestContainerAttr::None => Err(syn::Error::new( Span::call_site(), "missing `#[from_request(via(...))]`", @@ -72,18 +108,90 @@ pub(crate) fn expand(item: syn::Item) -> syn::Result { } } -fn error_on_generics(generics: syn::Generics) -> syn::Result<()> { - const GENERICS_ERROR: &str = "`#[derive(FromRequest)] doesn't support generics"; - - if !generics.params.is_empty() { - return Err(syn::Error::new_spanned(generics, GENERICS_ERROR)); - } - +fn parse_single_generic_type_on_struct( + generics: syn::Generics, + fields: &syn::Fields, +) -> syn::Result> { if let Some(where_clause) = generics.where_clause { - return Err(syn::Error::new_spanned(where_clause, GENERICS_ERROR)); + return Err(syn::Error::new_spanned( + where_clause, + "#[derive(FromRequest)] doesn't support structs with `where` clauses", + )); } - Ok(()) + match generics.params.len() { + 0 => Ok(None), + 1 => { + let param = generics.params.first().unwrap(); + let ty_ident = match param { + syn::GenericParam::Type(ty) => &ty.ident, + syn::GenericParam::Lifetime(lifetime) => { + return Err(syn::Error::new_spanned( + lifetime, + "#[derive(FromRequest)] doesn't support structs that are generic over lifetimes", + )); + } + syn::GenericParam::Const(konst) => { + return Err(syn::Error::new_spanned( + konst, + "#[derive(FromRequest)] doesn't support structs that have const generics", + )); + } + }; + + match fields { + syn::Fields::Named(fields_named) => { + return Err(syn::Error::new_spanned( + fields_named, + "#[derive(FromRequest)] doesn't support named fields for generic structs. Use a tuple struct instead", + )); + } + syn::Fields::Unnamed(fields_unnamed) => { + if fields_unnamed.unnamed.len() != 1 { + return Err(syn::Error::new_spanned( + fields_unnamed, + "#[derive(FromRequest)] only supports generics on tuple structs that have exactly one field", + )); + } + + let field = fields_unnamed.unnamed.first().unwrap(); + + if let syn::Type::Path(type_path) = &field.ty { + if type_path + .path + .get_ident() + .map_or(true, |field_type_ident| field_type_ident != ty_ident) + { + return Err(syn::Error::new_spanned( + type_path, + "#[derive(FromRequest)] only supports generics on tuple structs that have exactly one field of the generic type", + )); + } + } else { + return Err(syn::Error::new_spanned(&field.ty, "Expected type path")); + } + } + syn::Fields::Unit => return Ok(None), + } + + Ok(Some(ty_ident.clone())) + } + _ => Err(syn::Error::new_spanned( + generics, + "#[derive(FromRequest)] only supports 0 or 1 generic type parameters", + )), + } +} + +fn error_on_generic_ident(generic_ident: Option) -> syn::Result<()> { + if let Some(generic_ident) = generic_ident { + Err(syn::Error::new_spanned( + generic_ident, + "#[derive(FromRequest)] only supports generics when used with #[from_request(via)]", + )) + } else { + Ok(()) + } } fn impl_struct_by_extracting_each_field( @@ -91,10 +199,14 @@ fn impl_struct_by_extracting_each_field( fields: syn::Fields, vis: syn::Visibility, rejection_derive_opt_outs: RejectionDeriveOptOuts, + rejection: Option, ) -> syn::Result { - let extract_fields = extract_fields(&fields)?; + let extract_fields = extract_fields(&fields, &rejection)?; - let (rejection_ident, rejection) = if has_no_fields(&fields) { + let (rejection_ident, rejection) = if let Some(rejection) = rejection { + let rejection_ident = syn::parse_quote!(#rejection); + (rejection_ident, None) + } else if has_no_fields(&fields) { (syn::parse_quote!(::std::convert::Infallible), None) } else { let rejection_ident = rejection_ident(&ident); @@ -140,7 +252,10 @@ fn rejection_ident(ident: &syn::Ident) -> syn::Type { syn::parse_quote!(#ident) } -fn extract_fields(fields: &syn::Fields) -> syn::Result> { +fn extract_fields( + fields: &syn::Fields, + rejection: &Option, +) -> syn::Result> { fields .iter() .enumerate() @@ -190,12 +305,18 @@ fn extract_fields(fields: &syn::Fields) -> syn::Result> { }, }) } else { + let map_err = if let Some(rejection) = rejection { + quote! { <#rejection as ::std::convert::From<_>>::from } + } else { + quote! { Self::Rejection::#rejection_variant_name } + }; + Ok(quote_spanned! {ty_span=> #member: { ::axum::extract::FromRequest::from_request(req) .await .map(#into_inner) - .map_err(Self::Rejection::#rejection_variant_name)? + .map_err(#map_err)? }, }) } @@ -462,6 +583,8 @@ fn impl_struct_by_extracting_all_at_once( ident: syn::Ident, fields: syn::Fields, path: syn::Path, + rejection: Option, + generic_ident: Option, ) -> syn::Result { let fields = match fields { syn::Fields::Named(fields) => fields.named.into_iter(), @@ -482,23 +605,69 @@ fn impl_struct_by_extracting_all_at_once( let path_span = path.span(); + let associated_rejection_type = if let Some(rejection) = &rejection { + quote! { #rejection } + } else { + quote! { + <#path as ::axum::extract::FromRequest>::Rejection + } + }; + + let rejection_bound = rejection.as_ref().map(|rejection| { + if generic_ident.is_some() { + quote! { + #rejection: ::std::convert::From<<#path as ::axum::extract::FromRequest>::Rejection>, + } + } else { + quote! { + #rejection: ::std::convert::From<<#path as ::axum::extract::FromRequest>::Rejection>, + } + } + }).unwrap_or_default(); + + let impl_generics = if generic_ident.is_some() { + quote! { B, T } + } else { + quote! { B } + }; + + let type_generics = generic_ident + .is_some() + .then(|| quote! { }) + .unwrap_or_default(); + + let via_type_generics = if generic_ident.is_some() { + quote! { T } + } else { + quote! { Self } + }; + + let value_to_self = if generic_ident.is_some() { + quote! { + #ident(value) + } + } else { + quote! { value } + }; + Ok(quote_spanned! {path_span=> #[::axum::async_trait] #[automatically_derived] - impl ::axum::extract::FromRequest for #ident + impl<#impl_generics> ::axum::extract::FromRequest for #ident #type_generics where - B: ::axum::body::HttpBody + ::std::marker::Send + 'static, - B::Data: ::std::marker::Send, - B::Error: ::std::convert::Into<::axum::BoxError>, + #path<#via_type_generics>: ::axum::extract::FromRequest, + #rejection_bound + B: ::std::marker::Send, { - type Rejection = <#path as ::axum::extract::FromRequest>::Rejection; + type Rejection = #associated_rejection_type; async fn from_request( req: &mut ::axum::extract::RequestParts, ) -> ::std::result::Result { ::axum::extract::FromRequest::::from_request(req) .await - .map(|#path(inner)| inner) + .map(|#path(value)| #value_to_self) + .map_err(::std::convert::From::from) } } }) @@ -508,6 +677,7 @@ fn impl_enum_by_extracting_all_at_once( ident: syn::Ident, variants: Punctuated, path: syn::Path, + rejection: Option, ) -> syn::Result { for variant in variants { let FromRequestFieldAttr { via } = parse_field_attrs(&variant.attrs)?; @@ -535,6 +705,14 @@ fn impl_enum_by_extracting_all_at_once( } } + let associated_rejection_type = if let Some(rejection) = rejection { + quote! { #rejection } + } else { + quote! { + <#path as ::axum::extract::FromRequest>::Rejection + } + }; + let path_span = path.span(); Ok(quote_spanned! {path_span=> @@ -546,7 +724,7 @@ fn impl_enum_by_extracting_all_at_once( B::Data: ::std::marker::Send, B::Error: ::std::convert::Into<::axum::BoxError>, { - type Rejection = <#path as ::axum::extract::FromRequest>::Rejection; + type Rejection = #associated_rejection_type; async fn from_request( req: &mut ::axum::extract::RequestParts, @@ -554,6 +732,7 @@ fn impl_enum_by_extracting_all_at_once( ::axum::extract::FromRequest::::from_request(req) .await .map(|#path(inner)| inner) + .map_err(::std::convert::From::from) } } }) diff --git a/axum-macros/src/from_request/attr.rs b/axum-macros/src/from_request/attr.rs index 657c7bd6..46b2e518 100644 --- a/axum-macros/src/from_request/attr.rs +++ b/axum-macros/src/from_request/attr.rs @@ -11,7 +11,11 @@ pub(crate) struct FromRequestFieldAttr { } pub(crate) enum FromRequestContainerAttr { - Via(syn::Path), + Via { + path: syn::Path, + rejection: Option, + }, + Rejection(syn::Path), RejectionDerive(kw::rejection_derive, RejectionDeriveOptOuts), None, } @@ -19,6 +23,7 @@ pub(crate) enum FromRequestContainerAttr { pub(crate) mod kw { syn::custom_keyword!(via); syn::custom_keyword!(rejection_derive); + syn::custom_keyword!(rejection); syn::custom_keyword!(Display); syn::custom_keyword!(Debug); syn::custom_keyword!(Error); @@ -51,6 +56,7 @@ pub(crate) fn parse_container_attrs( let mut out_via = None; let mut out_rejection_derive = None; + let mut out_rejection = None; // we track the index of the attribute to know which comes last // used to give more accurate error messages @@ -73,11 +79,18 @@ pub(crate) fn parse_container_attrs( out_rejection_derive = Some((idx, rejection_derive, opt_outs)); } } + ContainerAttr::Rejection { rejection, path } => { + if out_rejection.is_some() { + return Err(double_attr_error("rejection", rejection)); + } else { + out_rejection = Some((idx, rejection, path)); + } + } } } - match (out_via, out_rejection_derive) { - (Some((via_idx, via, _)), Some((rejection_derive_idx, rejection_derive, _))) => { + match (out_via, out_rejection_derive, out_rejection) { + (Some((via_idx, via, _)), Some((rejection_derive_idx, rejection_derive, _)), _) => { if via_idx > rejection_derive_idx { Err(syn::Error::new_spanned( via, @@ -90,11 +103,41 @@ pub(crate) fn parse_container_attrs( )) } } - (Some((_, _, path)), None) => Ok(FromRequestContainerAttr::Via(path)), - (None, Some((_, rejection_derive, opt_outs))) => Ok( + + ( + _, + Some((rejection_derive_idx, rejection_derive, _)), + Some((rejection_idx, rejection, _)), + ) => { + if rejection_idx > rejection_derive_idx { + Err(syn::Error::new_spanned( + rejection, + "cannot use both `rejection_derive` and `rejection`", + )) + } else { + Err(syn::Error::new_spanned( + rejection_derive, + "cannot use both `rejection` and `rejection_derive`", + )) + } + } + + (Some((_, _, path)), None, None) => Ok(FromRequestContainerAttr::Via { + path, + rejection: None, + }), + (Some((_, _, path)), None, Some((_, _, rejection))) => Ok(FromRequestContainerAttr::Via { + path, + rejection: Some(rejection), + }), + + (None, Some((_, rejection_derive, opt_outs)), _) => Ok( FromRequestContainerAttr::RejectionDerive(rejection_derive, opt_outs), ), - (None, None) => Ok(FromRequestContainerAttr::None), + + (None, None, Some((_, _, rejection))) => Ok(FromRequestContainerAttr::Rejection(rejection)), + + (None, None, None) => Ok(FromRequestContainerAttr::None), } } @@ -125,6 +168,10 @@ enum ContainerAttr { via: kw::via, path: syn::Path, }, + Rejection { + rejection: kw::rejection, + path: syn::Path, + }, RejectionDerive { rejection_derive: kw::rejection_derive, opt_outs: RejectionDeriveOptOuts, @@ -147,6 +194,13 @@ impl Parse for ContainerAttr { rejection_derive, opt_outs, }) + } else if lh.peek(kw::rejection) { + let rejection = input.parse::()?; + let content; + syn::parenthesized!(content in input); + content + .parse() + .map(|path| Self::Rejection { rejection, path }) } else { Err(lh.error()) } diff --git a/axum-macros/src/lib.rs b/axum-macros/src/lib.rs index a44a14db..c76084d6 100644 --- a/axum-macros/src/lib.rs +++ b/axum-macros/src/lib.rs @@ -237,6 +237,56 @@ mod typed_path; /// } /// ``` /// +/// You can also use your own rejection type with `#[from_request(rejection(YourType))]`: +/// +/// ``` +/// use axum_macros::FromRequest; +/// use axum::{ +/// extract::{ +/// rejection::{ExtensionRejection, StringRejection}, +/// FromRequest, RequestParts, +/// }, +/// Extension, +/// response::{Response, IntoResponse}, +/// http::StatusCode, +/// headers::ContentType, +/// body::Bytes, +/// async_trait, +/// }; +/// +/// #[derive(FromRequest)] +/// #[from_request(rejection(MyRejection))] +/// struct MyExtractor { +/// state: Extension, +/// body: String, +/// } +/// +/// struct MyRejection(Response); +/// +/// // This tells axum how to convert `Extension`'s rejections into `MyRejection` +/// impl From for MyRejection { +/// fn from(rejection: ExtensionRejection) -> Self { +/// // ... +/// # todo!() +/// } +/// } +/// +/// // This tells axum how to convert `String`'s rejections into `MyRejection` +/// impl From for MyRejection { +/// fn from(rejection: StringRejection) -> Self { +/// // ... +/// # todo!() +/// } +/// } +/// +/// // All rejections must implement `IntoResponse` +/// impl IntoResponse for MyRejection { +/// fn into_response(self) -> Response { +/// self.0 +/// } +/// } +/// ``` +/// /// # The whole type at once /// /// By using `#[from_request(via(...))]` on the container you can extract the whole type at once, @@ -259,9 +309,101 @@ mod typed_path; /// The rejection will be the "via extractors"'s rejection. For the previous example that would be /// [`axum::extract::rejection::ExtensionRejection`]. /// +/// You can use a different rejection type with `#[from_request(rejection(YourType))]`: +/// +/// ``` +/// use axum_macros::FromRequest; +/// use axum::{ +/// extract::{Extension, rejection::ExtensionRejection}, +/// response::{IntoResponse, Response}, +/// Json, +/// http::StatusCode, +/// }; +/// use serde_json::json; +/// +/// // This will extracted via `Extension::::from_request` +/// #[derive(Clone, FromRequest)] +/// #[from_request( +/// via(Extension), +/// // Use your own rejection type +/// rejection(MyRejection), +/// )] +/// struct State { +/// // ... +/// } +/// +/// struct MyRejection(Response); +/// +/// // This tells axum how to convert `Extension`'s rejections into `MyRejection` +/// impl From for MyRejection { +/// fn from(rejection: ExtensionRejection) -> Self { +/// let response = ( +/// StatusCode::INTERNAL_SERVER_ERROR, +/// Json(json!({ "error": "Something went wrong..." })), +/// ).into_response(); +/// +/// MyRejection(response) +/// } +/// } +/// +/// // All rejections must implement `IntoResponse` +/// impl IntoResponse for MyRejection { +/// fn into_response(self) -> Response { +/// self.0 +/// } +/// } +/// +/// async fn handler(state: State) {} +/// ``` +/// +/// This allows you to wrap other extractors and easily customize the rejection: +/// +/// ``` +/// use axum_macros::FromRequest; +/// use axum::{ +/// extract::{Extension, rejection::JsonRejection}, +/// response::{IntoResponse, Response}, +/// http::StatusCode, +/// }; +/// use serde_json::json; +/// use serde::Deserialize; +/// +/// // create an extractor that internally uses `axum::Json` but has a custom rejection +/// #[derive(FromRequest)] +/// #[from_request(via(axum::Json), rejection(MyRejection))] +/// struct MyJson(T); +/// +/// struct MyRejection(Response); +/// +/// impl From for MyRejection { +/// fn from(rejection: JsonRejection) -> Self { +/// let response = ( +/// StatusCode::INTERNAL_SERVER_ERROR, +/// axum::Json(json!({ "error": rejection.to_string() })), +/// ).into_response(); +/// +/// MyRejection(response) +/// } +/// } +/// +/// impl IntoResponse for MyRejection { +/// fn into_response(self) -> Response { +/// self.0 +/// } +/// } +/// +/// #[derive(Deserialize)] +/// struct Payload {} +/// +/// async fn handler( +/// // make sure to use `MyJson` and not `axum::Json` +/// MyJson(payload): MyJson, +/// ) {} +/// ``` +/// /// # Known limitations /// -/// Generics are currently not supported: +/// Generics are only supported on tuple structs with exactly on field. Thus this doesn't work /// /// ```compile_fail /// #[derive(axum_macros::FromRequest)] diff --git a/axum-macros/tests/from_request/fail/generic.stderr b/axum-macros/tests/from_request/fail/generic.stderr index 51ca3ae3..151da867 100644 --- a/axum-macros/tests/from_request/fail/generic.stderr +++ b/axum-macros/tests/from_request/fail/generic.stderr @@ -1,5 +1,5 @@ -error: `#[derive(FromRequest)] doesn't support generics - --> tests/from_request/fail/generic.rs:4:17 +error: #[derive(FromRequest)] only supports generics on tuple structs that have exactly one field of the generic type + --> tests/from_request/fail/generic.rs:4:21 | 4 | struct Extractor(Option); - | ^^^ + | ^^^^^^^^^ diff --git a/axum-macros/tests/from_request/fail/generic_without_via.rs b/axum-macros/tests/from_request/fail/generic_without_via.rs new file mode 100644 index 00000000..29ec6095 --- /dev/null +++ b/axum-macros/tests/from_request/fail/generic_without_via.rs @@ -0,0 +1,11 @@ +use axum::{body::Body, routing::get, Extension, Router}; +use axum_macros::FromRequest; + +#[derive(FromRequest, Clone)] +struct Extractor(T); + +async fn foo(_: Extractor<()>) {} + +fn main() { + Router::::new().route("/", get(foo)); +} diff --git a/axum-macros/tests/from_request/fail/generic_without_via.stderr b/axum-macros/tests/from_request/fail/generic_without_via.stderr new file mode 100644 index 00000000..950779fe --- /dev/null +++ b/axum-macros/tests/from_request/fail/generic_without_via.stderr @@ -0,0 +1,29 @@ +error: #[derive(FromRequest)] only supports generics when used with #[from_request(via)] + --> tests/from_request/fail/generic_without_via.rs:5:18 + | +5 | struct Extractor(T); + | ^ + +warning: unused import: `Extension` + --> tests/from_request/fail/generic_without_via.rs:1:38 + | +1 | use axum::{body::Body, routing::get, Extension, Router}; + | ^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future {foo}: Handler<_, _>` is not satisfied + --> tests/from_request/fail/generic_without_via.rs:10:42 + | +10 | Router::::new().route("/", get(foo)); + | --- ^^^ the trait `Handler<_, _>` is not implemented for `fn(Extractor<()>) -> impl Future {foo}` + | | + | required by a bound introduced by this call + | + = help: the trait `Handler` is implemented for `Layered` +note: required by a bound in `axum::routing::get` + --> $WORKSPACE/axum/src/routing/method_routing.rs + | + | top_level_handler_fn!(get, GET); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `axum::routing::get` + = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/axum-macros/tests/from_request/fail/generic_without_via_rejection.rs b/axum-macros/tests/from_request/fail/generic_without_via_rejection.rs new file mode 100644 index 00000000..99bdf387 --- /dev/null +++ b/axum-macros/tests/from_request/fail/generic_without_via_rejection.rs @@ -0,0 +1,12 @@ +use axum::{body::Body, routing::get, Extension, Router}; +use axum_macros::FromRequest; + +#[derive(FromRequest, Clone)] +#[from_request(rejection(Foo))] +struct Extractor(T); + +async fn foo(_: Extractor<()>) {} + +fn main() { + Router::::new().route("/", get(foo)); +} diff --git a/axum-macros/tests/from_request/fail/generic_without_via_rejection.stderr b/axum-macros/tests/from_request/fail/generic_without_via_rejection.stderr new file mode 100644 index 00000000..199d3dd2 --- /dev/null +++ b/axum-macros/tests/from_request/fail/generic_without_via_rejection.stderr @@ -0,0 +1,29 @@ +error: #[derive(FromRequest)] only supports generics when used with #[from_request(via)] + --> tests/from_request/fail/generic_without_via_rejection.rs:6:18 + | +6 | struct Extractor(T); + | ^ + +warning: unused import: `Extension` + --> tests/from_request/fail/generic_without_via_rejection.rs:1:38 + | +1 | use axum::{body::Body, routing::get, Extension, Router}; + | ^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future {foo}: Handler<_, _>` is not satisfied + --> tests/from_request/fail/generic_without_via_rejection.rs:11:42 + | +11 | Router::::new().route("/", get(foo)); + | --- ^^^ the trait `Handler<_, _>` is not implemented for `fn(Extractor<()>) -> impl Future {foo}` + | | + | required by a bound introduced by this call + | + = help: the trait `Handler` is implemented for `Layered` +note: required by a bound in `axum::routing::get` + --> $WORKSPACE/axum/src/routing/method_routing.rs + | + | top_level_handler_fn!(get, GET); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `axum::routing::get` + = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/axum-macros/tests/from_request/fail/generic_without_via_rejection_derive.rs b/axum-macros/tests/from_request/fail/generic_without_via_rejection_derive.rs new file mode 100644 index 00000000..8ded0283 --- /dev/null +++ b/axum-macros/tests/from_request/fail/generic_without_via_rejection_derive.rs @@ -0,0 +1,12 @@ +use axum::{body::Body, routing::get, Extension, Router}; +use axum_macros::FromRequest; + +#[derive(FromRequest, Clone)] +#[from_request(rejection_derive(!Error))] +struct Extractor(T); + +async fn foo(_: Extractor<()>) {} + +fn main() { + Router::::new().route("/", get(foo)); +} diff --git a/axum-macros/tests/from_request/fail/generic_without_via_rejection_derive.stderr b/axum-macros/tests/from_request/fail/generic_without_via_rejection_derive.stderr new file mode 100644 index 00000000..5bc39b62 --- /dev/null +++ b/axum-macros/tests/from_request/fail/generic_without_via_rejection_derive.stderr @@ -0,0 +1,29 @@ +error: #[derive(FromRequest)] only supports generics when used with #[from_request(via)] + --> tests/from_request/fail/generic_without_via_rejection_derive.rs:6:18 + | +6 | struct Extractor(T); + | ^ + +warning: unused import: `Extension` + --> tests/from_request/fail/generic_without_via_rejection_derive.rs:1:38 + | +1 | use axum::{body::Body, routing::get, Extension, Router}; + | ^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` on by default + +error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future {foo}: Handler<_, _>` is not satisfied + --> tests/from_request/fail/generic_without_via_rejection_derive.rs:11:42 + | +11 | Router::::new().route("/", get(foo)); + | --- ^^^ the trait `Handler<_, _>` is not implemented for `fn(Extractor<()>) -> impl Future {foo}` + | | + | required by a bound introduced by this call + | + = help: the trait `Handler` is implemented for `Layered` +note: required by a bound in `axum::routing::get` + --> $WORKSPACE/axum/src/routing/method_routing.rs + | + | top_level_handler_fn!(get, GET); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `axum::routing::get` + = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/axum-macros/tests/from_request/fail/override_rejection_on_enum_without_via.rs b/axum-macros/tests/from_request/fail/override_rejection_on_enum_without_via.rs new file mode 100644 index 00000000..e855d5f6 --- /dev/null +++ b/axum-macros/tests/from_request/fail/override_rejection_on_enum_without_via.rs @@ -0,0 +1,33 @@ +use axum::{ + extract::rejection::ExtensionRejection, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use axum_macros::FromRequest; + +fn main() { + let _: Router = Router::new().route("/", get(handler).post(handler_result)); +} + +async fn handler(_: MyExtractor) {} + +async fn handler_result(_: Result) {} + +#[derive(FromRequest, Clone)] +#[from_request(rejection(MyRejection))] +enum MyExtractor {} + +struct MyRejection {} + +impl From for MyRejection { + fn from(_: ExtensionRejection) -> Self { + todo!() + } +} + +impl IntoResponse for MyRejection { + fn into_response(self) -> Response { + todo!() + } +} diff --git a/axum-macros/tests/from_request/fail/override_rejection_on_enum_without_via.stderr b/axum-macros/tests/from_request/fail/override_rejection_on_enum_without_via.stderr new file mode 100644 index 00000000..1bd8a29a --- /dev/null +++ b/axum-macros/tests/from_request/fail/override_rejection_on_enum_without_via.stderr @@ -0,0 +1,37 @@ +error: cannot use `rejection` without `via` + --> tests/from_request/fail/override_rejection_on_enum_without_via.rs:18:26 + | +18 | #[from_request(rejection(MyRejection))] + | ^^^^^^^^^^^ + +error[E0277]: the trait bound `fn(MyExtractor) -> impl Future {handler}: Handler<_, _>` is not satisfied + --> tests/from_request/fail/override_rejection_on_enum_without_via.rs:10:50 + | +10 | let _: Router = Router::new().route("/", get(handler).post(handler_result)); + | --- ^^^^^^^ the trait `Handler<_, _>` is not implemented for `fn(MyExtractor) -> impl Future {handler}` + | | + | required by a bound introduced by this call + | + = help: the trait `Handler` is implemented for `Layered` +note: required by a bound in `axum::routing::get` + --> $WORKSPACE/axum/src/routing/method_routing.rs + | + | top_level_handler_fn!(get, GET); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `axum::routing::get` + = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `fn(Result) -> impl Future {handler_result}: Handler<_, _>` is not satisfied + --> tests/from_request/fail/override_rejection_on_enum_without_via.rs:10:64 + | +10 | let _: Router = Router::new().route("/", get(handler).post(handler_result)); + | ---- ^^^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for `fn(Result) -> impl Future {handler_result}` + | | + | required by a bound introduced by this call + | + = help: the trait `Handler` is implemented for `Layered` +note: required by a bound in `MethodRouter::::post` + --> $WORKSPACE/axum/src/routing/method_routing.rs + | + | chained_handler_fn!(post, POST); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `MethodRouter::::post` + = note: this error originates in the macro `chained_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/axum-macros/tests/from_request/fail/unknown_attr_container.stderr b/axum-macros/tests/from_request/fail/unknown_attr_container.stderr index 10608bb6..9d89c085 100644 --- a/axum-macros/tests/from_request/fail/unknown_attr_container.stderr +++ b/axum-macros/tests/from_request/fail/unknown_attr_container.stderr @@ -1,4 +1,4 @@ -error: expected `via` or `rejection_derive` +error: expected one of: `via`, `rejection_derive`, `rejection` --> tests/from_request/fail/unknown_attr_container.rs:4:16 | 4 | #[from_request(foo)] diff --git a/axum-macros/tests/from_request/pass/override_rejection.rs b/axum-macros/tests/from_request/pass/override_rejection.rs new file mode 100644 index 00000000..431474b8 --- /dev/null +++ b/axum-macros/tests/from_request/pass/override_rejection.rs @@ -0,0 +1,61 @@ +use axum::{ + async_trait, + extract::{rejection::ExtensionRejection, FromRequest, RequestParts}, + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, + Extension, Router, +}; +use axum_macros::FromRequest; + +fn main() { + let _: Router = Router::new().route("/", get(handler).post(handler_result)); +} + +async fn handler(_: MyExtractor) {} + +async fn handler_result(_: Result) {} + +#[derive(FromRequest)] +#[from_request(rejection(MyRejection))] +struct MyExtractor { + one: Extension, + #[from_request(via(Extension))] + two: String, + three: OtherExtractor, +} + +struct OtherExtractor; + +#[async_trait] +impl FromRequest 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) -> Result { + todo!() + } +} + +struct MyRejection {} + +impl From for MyRejection { + fn from(_: ExtensionRejection) -> Self { + todo!() + } +} + +impl From<(StatusCode, String)> for MyRejection { + fn from(_: (StatusCode, String)) -> Self { + todo!() + } +} + +impl IntoResponse for MyRejection { + fn into_response(self) -> Response { + todo!() + } +} diff --git a/axum-macros/tests/from_request/pass/override_rejection_with_via_on_enum.rs b/axum-macros/tests/from_request/pass/override_rejection_with_via_on_enum.rs new file mode 100644 index 00000000..1aabfbb3 --- /dev/null +++ b/axum-macros/tests/from_request/pass/override_rejection_with_via_on_enum.rs @@ -0,0 +1,33 @@ +use axum::{ + extract::rejection::ExtensionRejection, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use axum_macros::FromRequest; + +fn main() { + let _: Router = Router::new().route("/", get(handler).post(handler_result)); +} + +async fn handler(_: MyExtractor) {} + +async fn handler_result(_: Result) {} + +#[derive(FromRequest, Clone)] +#[from_request(via(axum::Extension), rejection(MyRejection))] +enum MyExtractor {} + +struct MyRejection {} + +impl From for MyRejection { + fn from(_: ExtensionRejection) -> Self { + todo!() + } +} + +impl IntoResponse for MyRejection { + fn into_response(self) -> Response { + todo!() + } +} diff --git a/axum-macros/tests/from_request/pass/override_rejection_with_via_on_struct.rs b/axum-macros/tests/from_request/pass/override_rejection_with_via_on_struct.rs new file mode 100644 index 00000000..2a046fae --- /dev/null +++ b/axum-macros/tests/from_request/pass/override_rejection_with_via_on_struct.rs @@ -0,0 +1,40 @@ +use axum::{ + extract::rejection::JsonRejection, + response::{IntoResponse, Response}, + routing::get, + Router, +}; +use axum_macros::FromRequest; +use serde::Deserialize; + +fn main() { + let _: Router = Router::new().route("/", get(handler).post(handler_result)); +} + +#[derive(Deserialize)] +struct Payload {} + +async fn handler(_: MyJson) {} + +async fn handler_result(_: Result, MyJsonRejection>) {} + +#[derive(FromRequest)] +#[from_request( + via(axum::Json), + rejection(MyJsonRejection), +)] +struct MyJson(T); + +struct MyJsonRejection {} + +impl From for MyJsonRejection { + fn from(_: JsonRejection) -> Self { + todo!() + } +} + +impl IntoResponse for MyJsonRejection { + fn into_response(self) -> Response { + todo!() + } +}