mirror of
https://github.com/tokio-rs/axum.git
synced 2025-01-11 12:31:25 +01:00
Support using a different rejection for #[derive(FromRequest)]
(#1256)
This commit is contained in:
parent
a8e80bcb97
commit
ac7037d282
18 changed files with 750 additions and 45 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<TokenStream> {
|
|||
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<TokenStream> {
|
|||
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<TokenStream> {
|
|||
"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<TokenStream> {
|
|||
}
|
||||
}
|
||||
|
||||
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<Option<Ident>> {
|
||||
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<Ident>) -> 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::Path>,
|
||||
) -> syn::Result<TokenStream> {
|
||||
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<Vec<TokenStream>> {
|
||||
fn extract_fields(
|
||||
fields: &syn::Fields,
|
||||
rejection: &Option<syn::Path>,
|
||||
) -> syn::Result<Vec<TokenStream>> {
|
||||
fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
|
@ -190,12 +305,18 @@ fn extract_fields(fields: &syn::Fields) -> syn::Result<Vec<TokenStream>> {
|
|||
},
|
||||
})
|
||||
} 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<syn::Path>,
|
||||
generic_ident: Option<Ident>,
|
||||
) -> syn::Result<TokenStream> {
|
||||
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<Self> as ::axum::extract::FromRequest<B>>::Rejection
|
||||
}
|
||||
};
|
||||
|
||||
let rejection_bound = rejection.as_ref().map(|rejection| {
|
||||
if generic_ident.is_some() {
|
||||
quote! {
|
||||
#rejection: ::std::convert::From<<#path<T> as ::axum::extract::FromRequest<B>>::Rejection>,
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#rejection: ::std::convert::From<<#path<Self> as ::axum::extract::FromRequest<B>>::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! { <T> })
|
||||
.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<B> ::axum::extract::FromRequest<B> for #ident
|
||||
impl<#impl_generics> ::axum::extract::FromRequest<B> 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<B>,
|
||||
#rejection_bound
|
||||
B: ::std::marker::Send,
|
||||
{
|
||||
type Rejection = <#path<Self> as ::axum::extract::FromRequest<B>>::Rejection;
|
||||
type Rejection = #associated_rejection_type;
|
||||
|
||||
async fn from_request(
|
||||
req: &mut ::axum::extract::RequestParts<B>,
|
||||
) -> ::std::result::Result<Self, Self::Rejection> {
|
||||
::axum::extract::FromRequest::<B>::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<syn::Variant, Token![,]>,
|
||||
path: syn::Path,
|
||||
rejection: Option<syn::Path>,
|
||||
) -> syn::Result<TokenStream> {
|
||||
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<Self> as ::axum::extract::FromRequest<B>>::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<Self> as ::axum::extract::FromRequest<B>>::Rejection;
|
||||
type Rejection = #associated_rejection_type;
|
||||
|
||||
async fn from_request(
|
||||
req: &mut ::axum::extract::RequestParts<B>,
|
||||
|
@ -554,6 +732,7 @@ fn impl_enum_by_extracting_all_at_once(
|
|||
::axum::extract::FromRequest::<B>::from_request(req)
|
||||
.await
|
||||
.map(|#path(inner)| inner)
|
||||
.map_err(::std::convert::From::from)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -11,7 +11,11 @@ pub(crate) struct FromRequestFieldAttr {
|
|||
}
|
||||
|
||||
pub(crate) enum FromRequestContainerAttr {
|
||||
Via(syn::Path),
|
||||
Via {
|
||||
path: syn::Path,
|
||||
rejection: Option<syn::Path>,
|
||||
},
|
||||
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::<kw::rejection>()?;
|
||||
let content;
|
||||
syn::parenthesized!(content in input);
|
||||
content
|
||||
.parse()
|
||||
.map(|path| Self::Rejection { rejection, path })
|
||||
} else {
|
||||
Err(lh.error())
|
||||
}
|
||||
|
|
|
@ -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<String>,
|
||||
/// body: String,
|
||||
/// }
|
||||
///
|
||||
/// struct MyRejection(Response);
|
||||
///
|
||||
/// // This tells axum how to convert `Extension`'s rejections into `MyRejection`
|
||||
/// impl From<ExtensionRejection> for MyRejection {
|
||||
/// fn from(rejection: ExtensionRejection) -> Self {
|
||||
/// // ...
|
||||
/// # todo!()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // This tells axum how to convert `String`'s rejections into `MyRejection`
|
||||
/// impl From<StringRejection> 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::<State>::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<ExtensionRejection> 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>(T);
|
||||
///
|
||||
/// struct MyRejection(Response);
|
||||
///
|
||||
/// impl From<JsonRejection> 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<Payload>,
|
||||
/// ) {}
|
||||
/// ```
|
||||
///
|
||||
/// # 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)]
|
||||
|
|
|
@ -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<T>(Option<T>);
|
||||
| ^^^
|
||||
| ^^^^^^^^^
|
||||
|
|
11
axum-macros/tests/from_request/fail/generic_without_via.rs
Normal file
11
axum-macros/tests/from_request/fail/generic_without_via.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use axum::{body::Body, routing::get, Extension, Router};
|
||||
use axum_macros::FromRequest;
|
||||
|
||||
#[derive(FromRequest, Clone)]
|
||||
struct Extractor<T>(T);
|
||||
|
||||
async fn foo(_: Extractor<()>) {}
|
||||
|
||||
fn main() {
|
||||
Router::<Body>::new().route("/", get(foo));
|
||||
}
|
|
@ -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>(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<Output = ()> {foo}: Handler<_, _>` is not satisfied
|
||||
--> tests/from_request/fail/generic_without_via.rs:10:42
|
||||
|
|
||||
10 | Router::<Body>::new().route("/", get(foo));
|
||||
| --- ^^^ the trait `Handler<_, _>` is not implemented for `fn(Extractor<()>) -> impl Future<Output = ()> {foo}`
|
||||
| |
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= help: the trait `Handler<T, ReqBody>` is implemented for `Layered<S, T>`
|
||||
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)
|
|
@ -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>(T);
|
||||
|
||||
async fn foo(_: Extractor<()>) {}
|
||||
|
||||
fn main() {
|
||||
Router::<Body>::new().route("/", get(foo));
|
||||
}
|
|
@ -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>(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<Output = ()> {foo}: Handler<_, _>` is not satisfied
|
||||
--> tests/from_request/fail/generic_without_via_rejection.rs:11:42
|
||||
|
|
||||
11 | Router::<Body>::new().route("/", get(foo));
|
||||
| --- ^^^ the trait `Handler<_, _>` is not implemented for `fn(Extractor<()>) -> impl Future<Output = ()> {foo}`
|
||||
| |
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= help: the trait `Handler<T, ReqBody>` is implemented for `Layered<S, T>`
|
||||
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)
|
|
@ -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>(T);
|
||||
|
||||
async fn foo(_: Extractor<()>) {}
|
||||
|
||||
fn main() {
|
||||
Router::<Body>::new().route("/", get(foo));
|
||||
}
|
|
@ -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>(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<Output = ()> {foo}: Handler<_, _>` is not satisfied
|
||||
--> tests/from_request/fail/generic_without_via_rejection_derive.rs:11:42
|
||||
|
|
||||
11 | Router::<Body>::new().route("/", get(foo));
|
||||
| --- ^^^ the trait `Handler<_, _>` is not implemented for `fn(Extractor<()>) -> impl Future<Output = ()> {foo}`
|
||||
| |
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= help: the trait `Handler<T, ReqBody>` is implemented for `Layered<S, T>`
|
||||
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)
|
|
@ -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<MyExtractor, MyRejection>) {}
|
||||
|
||||
#[derive(FromRequest, Clone)]
|
||||
#[from_request(rejection(MyRejection))]
|
||||
enum MyExtractor {}
|
||||
|
||||
struct MyRejection {}
|
||||
|
||||
impl From<ExtensionRejection> for MyRejection {
|
||||
fn from(_: ExtensionRejection) -> Self {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for MyRejection {
|
||||
fn into_response(self) -> Response {
|
||||
todo!()
|
||||
}
|
||||
}
|
|
@ -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<Output = ()> {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<Output = ()> {handler}`
|
||||
| |
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= help: the trait `Handler<T, ReqBody>` is implemented for `Layered<S, T>`
|
||||
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<MyExtractor, MyRejection>) -> impl Future<Output = ()> {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<MyExtractor, MyRejection>) -> impl Future<Output = ()> {handler_result}`
|
||||
| |
|
||||
| required by a bound introduced by this call
|
||||
|
|
||||
= help: the trait `Handler<T, ReqBody>` is implemented for `Layered<S, T>`
|
||||
note: required by a bound in `MethodRouter::<B>::post`
|
||||
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
||||
|
|
||||
| chained_handler_fn!(post, POST);
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `MethodRouter::<B>::post`
|
||||
= note: this error originates in the macro `chained_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
@ -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)]
|
||||
|
|
61
axum-macros/tests/from_request/pass/override_rejection.rs
Normal file
61
axum-macros/tests/from_request/pass/override_rejection.rs
Normal file
|
@ -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<MyExtractor, MyRejection>) {}
|
||||
|
||||
#[derive(FromRequest)]
|
||||
#[from_request(rejection(MyRejection))]
|
||||
struct MyExtractor {
|
||||
one: Extension<String>,
|
||||
#[from_request(via(Extension))]
|
||||
two: String,
|
||||
three: 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> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
struct MyRejection {}
|
||||
|
||||
impl From<ExtensionRejection> 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!()
|
||||
}
|
||||
}
|
|
@ -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<MyExtractor, MyRejection>) {}
|
||||
|
||||
#[derive(FromRequest, Clone)]
|
||||
#[from_request(via(axum::Extension), rejection(MyRejection))]
|
||||
enum MyExtractor {}
|
||||
|
||||
struct MyRejection {}
|
||||
|
||||
impl From<ExtensionRejection> for MyRejection {
|
||||
fn from(_: ExtensionRejection) -> Self {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for MyRejection {
|
||||
fn into_response(self) -> Response {
|
||||
todo!()
|
||||
}
|
||||
}
|
|
@ -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<Payload>) {}
|
||||
|
||||
async fn handler_result(_: Result<MyJson<Payload>, MyJsonRejection>) {}
|
||||
|
||||
#[derive(FromRequest)]
|
||||
#[from_request(
|
||||
via(axum::Json),
|
||||
rejection(MyJsonRejection),
|
||||
)]
|
||||
struct MyJson<T>(T);
|
||||
|
||||
struct MyJsonRejection {}
|
||||
|
||||
impl From<JsonRejection> for MyJsonRejection {
|
||||
fn from(_: JsonRejection) -> Self {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for MyJsonRejection {
|
||||
fn into_response(self) -> Response {
|
||||
todo!()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue