mirror of
https://github.com/tokio-rs/axum.git
synced 2024-12-29 15:49:16 +01:00
Generate correct bound for non-last extractor in #[debug_handler]
(#1299)
* Support running a single UI test * Generate correct `FromRequestParts` bound * don't depend on itertools
This commit is contained in:
parent
be624306f4
commit
bb05dea672
9 changed files with 214 additions and 45 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::with_position::{Position, WithPosition};
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::{format_ident, quote, quote_spanned};
|
use quote::{format_ident, quote, quote_spanned};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
@ -173,12 +174,16 @@ fn check_inputs_impls_from_request(
|
||||||
FnArg::Typed(typed) => is_self_pat_type(typed),
|
FnArg::Typed(typed) => is_self_pat_type(typed),
|
||||||
});
|
});
|
||||||
|
|
||||||
item_fn
|
WithPosition::new(item_fn.sig.inputs.iter())
|
||||||
.sig
|
|
||||||
.inputs
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, arg)| {
|
.map(|(idx, arg)| {
|
||||||
|
let must_impl_from_request_parts = match &arg {
|
||||||
|
Position::First(_) | Position::Middle(_) => true,
|
||||||
|
Position::Last(_) | Position::Only(_) => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let arg = arg.into_inner();
|
||||||
|
|
||||||
let (span, ty) = match arg {
|
let (span, ty) = match arg {
|
||||||
FnArg::Receiver(receiver) => {
|
FnArg::Receiver(receiver) => {
|
||||||
if receiver.reference.is_some() {
|
if receiver.reference.is_some() {
|
||||||
|
@ -228,11 +233,27 @@ fn check_inputs_impls_from_request(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let check_fn_generics = if must_impl_from_request_parts {
|
||||||
|
quote! {}
|
||||||
|
} else {
|
||||||
|
quote! { <M> }
|
||||||
|
};
|
||||||
|
|
||||||
|
let from_request_bound = if must_impl_from_request_parts {
|
||||||
|
quote! {
|
||||||
|
#ty: ::axum::extract::FromRequestParts<#state_ty> + Send
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
#ty: ::axum::extract::FromRequest<#state_ty, #body_ty, M> + Send
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
quote_spanned! {span=>
|
quote_spanned! {span=>
|
||||||
#[allow(warnings)]
|
#[allow(warnings)]
|
||||||
fn #check_fn<M>()
|
fn #check_fn #check_fn_generics()
|
||||||
where
|
where
|
||||||
#ty: ::axum::extract::FromRequest<#state_ty, #body_ty, M> + Send,
|
#from_request_bound,
|
||||||
{}
|
{}
|
||||||
|
|
||||||
// we have to call the function to actually trigger a compile error
|
// we have to call the function to actually trigger a compile error
|
||||||
|
@ -472,15 +493,5 @@ fn state_type_from_args(item_fn: &ItemFn) -> Option<Type> {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui() {
|
fn ui() {
|
||||||
#[rustversion::stable]
|
crate::run_ui_tests("debug_handler");
|
||||||
fn go() {
|
|
||||||
let t = trybuild::TestCases::new();
|
|
||||||
t.compile_fail("tests/debug_handler/fail/*.rs");
|
|
||||||
t.pass("tests/debug_handler/pass/*.rs");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustversion::not(stable)]
|
|
||||||
fn go() {}
|
|
||||||
|
|
||||||
go();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -564,17 +564,7 @@ fn impl_enum_by_extracting_all_at_once(
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui() {
|
fn ui() {
|
||||||
#[rustversion::stable]
|
crate::run_ui_tests("from_request");
|
||||||
fn go() {
|
|
||||||
let t = trybuild::TestCases::new();
|
|
||||||
t.compile_fail("tests/from_request/fail/*.rs");
|
|
||||||
t.pass("tests/from_request/pass/*.rs");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustversion::not(stable)]
|
|
||||||
fn go() {}
|
|
||||||
|
|
||||||
go();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For some reason the compiler error for this is different locally and on CI. No idea why... So
|
/// For some reason the compiler error for this is different locally and on CI. No idea why... So
|
||||||
|
|
|
@ -50,6 +50,7 @@ use syn::parse::Parse;
|
||||||
mod debug_handler;
|
mod debug_handler;
|
||||||
mod from_request;
|
mod from_request;
|
||||||
mod typed_path;
|
mod typed_path;
|
||||||
|
mod with_position;
|
||||||
|
|
||||||
/// Derive an implementation of [`FromRequest`].
|
/// Derive an implementation of [`FromRequest`].
|
||||||
///
|
///
|
||||||
|
@ -561,3 +562,37 @@ where
|
||||||
Err(err) => err.into_compile_error().into(),
|
Err(err) => err.into_compile_error().into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn run_ui_tests(directory: &str) {
|
||||||
|
#[rustversion::stable]
|
||||||
|
fn go(directory: &str) {
|
||||||
|
let t = trybuild::TestCases::new();
|
||||||
|
|
||||||
|
if let Ok(mut path) = std::env::var("AXUM_TEST_ONLY") {
|
||||||
|
if let Some(path_without_prefix) = path.strip_prefix("axum-macros/") {
|
||||||
|
path = path_without_prefix.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !path.contains(&format!("/{}/", directory)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.contains("/fail/") {
|
||||||
|
t.compile_fail(path);
|
||||||
|
} else if path.contains("/pass/") {
|
||||||
|
t.pass(path);
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.compile_fail(format!("tests/{}/fail/*.rs", directory));
|
||||||
|
t.pass(format!("tests/{}/pass/*.rs", directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustversion::not(stable)]
|
||||||
|
fn go(directory: &str) {}
|
||||||
|
|
||||||
|
go(directory);
|
||||||
|
}
|
||||||
|
|
|
@ -423,15 +423,5 @@ fn map_err_rejection(rejection: &Option<syn::Path>) -> TokenStream {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ui() {
|
fn ui() {
|
||||||
#[rustversion::stable]
|
crate::run_ui_tests("typed_path");
|
||||||
fn go() {
|
|
||||||
let t = trybuild::TestCases::new();
|
|
||||||
t.compile_fail("tests/typed_path/fail/*.rs");
|
|
||||||
t.pass("tests/typed_path/pass/*.rs");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustversion::not(stable)]
|
|
||||||
fn go() {}
|
|
||||||
|
|
||||||
go();
|
|
||||||
}
|
}
|
||||||
|
|
116
axum-macros/src/with_position.rs
Normal file
116
axum-macros/src/with_position.rs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
// this is copied from itertools under the following license
|
||||||
|
//
|
||||||
|
// Copyright (c) 2015
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any
|
||||||
|
// person obtaining a copy of this software and associated
|
||||||
|
// documentation files (the "Software"), to deal in the
|
||||||
|
// Software without restriction, including without
|
||||||
|
// limitation the rights to use, copy, modify, merge,
|
||||||
|
// publish, distribute, sublicense, and/or sell copies of
|
||||||
|
// the Software, and to permit persons to whom the Software
|
||||||
|
// is furnished to do so, subject to the following
|
||||||
|
// conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice
|
||||||
|
// shall be included in all copies or substantial portions
|
||||||
|
// of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
// DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
use std::iter::{Fuse, FusedIterator, Peekable};
|
||||||
|
|
||||||
|
pub(crate) struct WithPosition<I>
|
||||||
|
where
|
||||||
|
I: Iterator,
|
||||||
|
{
|
||||||
|
handled_first: bool,
|
||||||
|
peekable: Peekable<Fuse<I>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> WithPosition<I>
|
||||||
|
where
|
||||||
|
I: Iterator,
|
||||||
|
{
|
||||||
|
pub(crate) fn new(iter: I) -> WithPosition<I> {
|
||||||
|
WithPosition {
|
||||||
|
handled_first: false,
|
||||||
|
peekable: iter.fuse().peekable(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> Clone for WithPosition<I>
|
||||||
|
where
|
||||||
|
I: Clone + Iterator,
|
||||||
|
I::Item: Clone,
|
||||||
|
{
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
handled_first: self.handled_first,
|
||||||
|
peekable: self.peekable.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
pub(crate) enum Position<T> {
|
||||||
|
First(T),
|
||||||
|
Middle(T),
|
||||||
|
Last(T),
|
||||||
|
Only(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Position<T> {
|
||||||
|
pub(crate) fn into_inner(self) -> T {
|
||||||
|
match self {
|
||||||
|
Position::First(x) | Position::Middle(x) | Position::Last(x) | Position::Only(x) => x,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Iterator> Iterator for WithPosition<I> {
|
||||||
|
type Item = Position<I::Item>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.peekable.next() {
|
||||||
|
Some(item) => {
|
||||||
|
if !self.handled_first {
|
||||||
|
// Haven't seen the first item yet, and there is one to give.
|
||||||
|
self.handled_first = true;
|
||||||
|
// Peek to see if this is also the last item,
|
||||||
|
// in which case tag it as `Only`.
|
||||||
|
match self.peekable.peek() {
|
||||||
|
Some(_) => Some(Position::First(item)),
|
||||||
|
None => Some(Position::Only(item)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Have seen the first item, and there's something left.
|
||||||
|
// Peek to see if this is the last item.
|
||||||
|
match self.peekable.peek() {
|
||||||
|
Some(_) => Some(Position::Middle(item)),
|
||||||
|
None => Some(Position::Last(item)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Iterator is finished.
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.peekable.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> ExactSizeIterator for WithPosition<I> where I: ExactSizeIterator {}
|
||||||
|
|
||||||
|
impl<I: Iterator> FusedIterator for WithPosition<I> {}
|
|
@ -16,7 +16,10 @@ error[E0277]: the trait bound `bool: FromRequestParts<()>` is not satisfied
|
||||||
and 26 others
|
and 26 others
|
||||||
= note: required because of the requirements on the impl of `FromRequest<(), Body, axum_core::extract::private::ViaParts>` for `bool`
|
= note: required because of the requirements on the impl of `FromRequest<(), Body, axum_core::extract::private::ViaParts>` for `bool`
|
||||||
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
|
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`
|
||||||
--> tests/debug_handler/fail/argument_not_extractor.rs:4:23
|
--> tests/debug_handler/fail/argument_not_extractor.rs:3:1
|
||||||
|
|
|
|
||||||
|
3 | #[debug_handler]
|
||||||
|
| ^^^^^^^^^^^^^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`
|
||||||
4 | async fn handler(foo: bool) {}
|
4 | async fn handler(foo: bool) {}
|
||||||
| ^^^^ required by this bound in `__axum_macros_check_handler_0_from_request_check`
|
| ---- required by a bound in this
|
||||||
|
= note: this error originates in the attribute macro `debug_handler` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
use axum_macros::debug_handler;
|
||||||
|
use axum::http::Method;
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
async fn handler(_: String, _: Method) {}
|
||||||
|
|
||||||
|
fn main() {}
|
|
@ -0,0 +1,18 @@
|
||||||
|
error[E0277]: the trait bound `String: FromRequestParts<()>` is not satisfied
|
||||||
|
--> tests/debug_handler/fail/doesnt_implement_from_request_parts.rs:4:1
|
||||||
|
|
|
||||||
|
4 | #[debug_handler]
|
||||||
|
| ^^^^^^^^^^^^^^^^ the trait `FromRequestParts<()>` is not implemented for `String`
|
||||||
|
|
|
||||||
|
= help: the following other types implement trait `FromRequestParts<S>`:
|
||||||
|
<() as FromRequestParts<S>>
|
||||||
|
<(T1, T2) as FromRequestParts<S>>
|
||||||
|
<(T1, T2, T3) as FromRequestParts<S>>
|
||||||
|
<(T1, T2, T3, T4) as FromRequestParts<S>>
|
||||||
|
<(T1, T2, T3, T4, T5) as FromRequestParts<S>>
|
||||||
|
<(T1, T2, T3, T4, T5, T6) as FromRequestParts<S>>
|
||||||
|
<(T1, T2, T3, T4, T5, T6, T7) as FromRequestParts<S>>
|
||||||
|
<(T1, T2, T3, T4, T5, T6, T7, T8) as FromRequestParts<S>>
|
||||||
|
and 26 others
|
||||||
|
= help: see issue #48214
|
||||||
|
= note: this error originates in the attribute macro `debug_handler` (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
@ -1,4 +1,4 @@
|
||||||
use axum::{async_trait, extract::FromRequest, http::Request, response::IntoResponse};
|
use axum::{async_trait, extract::FromRequestParts, http::request::Parts, response::IntoResponse};
|
||||||
use axum_macros::debug_handler;
|
use axum_macros::debug_handler;
|
||||||
|
|
||||||
fn main() {}
|
fn main() {}
|
||||||
|
@ -116,14 +116,13 @@ impl A {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S, B> FromRequest<S, B> for A
|
impl<S> FromRequestParts<S> for A
|
||||||
where
|
where
|
||||||
B: Send + 'static,
|
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
{
|
{
|
||||||
type Rejection = ();
|
type Rejection = ();
|
||||||
|
|
||||||
async fn from_request(_req: Request<B>, _state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(_parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue