Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
use quote::ToTokens;
use syn::{
parse::{Parse, ParseStream},
Token,
};
pub(crate) fn parse_parenthesized_attribute<K, T>(
input: ParseStream<'_>,
out: &mut Option<(K, T)>,
) -> syn::Result<()>
where
K: Parse + ToTokens,
T: Parse,
{
let kw = input.parse()?;
let content;
syn::parenthesized!(content in input);
let inner = content.parse()?;
if out.is_some() {
let kw_name = std::any::type_name::<K>().split("::").last().unwrap();
let msg = format!("`{kw_name}` specified more than once");
return Err(syn::Error::new_spanned(kw, msg));
}
*out = Some((kw, inner));
Ok(())
}
pub(crate) fn parse_assignment_attribute<K, T>(
input: ParseStream<'_>,
out: &mut Option<(K, T)>,
) -> syn::Result<()>
where
K: Parse + ToTokens,
T: Parse,
{
let kw = input.parse()?;
input.parse::<Token![=]>()?;
let inner = input.parse()?;
if out.is_some() {
let kw_name = std::any::type_name::<K>().split("::").last().unwrap();
let msg = format!("`{kw_name}` specified more than once");
return Err(syn::Error::new_spanned(kw, msg));
}
*out = Some((kw, inner));
Ok(())
}
pub(crate) trait Combine: Sized {
fn combine(self, other: Self) -> syn::Result<Self>;
}
pub(crate) fn parse_attrs<T>(ident: &str, attrs: &[syn::Attribute]) -> syn::Result<T>
where
T: Combine + Default + Parse,
{
attrs
.iter()
.filter(|attr| attr.meta.path().is_ident(ident))
.map(|attr| attr.parse_args::<T>())
.try_fold(T::default(), |out, next| out.combine(next?))
}
pub(crate) fn combine_attribute<K, T>(a: &mut Option<(K, T)>, b: Option<(K, T)>) -> syn::Result<()>
where
K: ToTokens,
{
if let Some((kw, inner)) = b {
if a.is_some() {
let kw_name = std::any::type_name::<K>().split("::").last().unwrap();
let msg = format!("`{kw_name}` specified more than once");
return Err(syn::Error::new_spanned(kw, msg));
}
*a = Some((kw, inner));
}
Ok(())
}
pub(crate) fn combine_unary_attribute<K>(a: &mut Option<K>, b: Option<K>) -> syn::Result<()>
where
K: ToTokens,
{
if let Some(kw) = b {
if a.is_some() {
let kw_name = std::any::type_name::<K>().split("::").last().unwrap();
let msg = format!("`{kw_name}` specified more than once");
return Err(syn::Error::new_spanned(kw, msg));
}
*a = Some(kw);
}
Ok(())
}
pub(crate) fn second<T, K>(tuple: (T, K)) -> K {
tuple.1
}
+45
View File
@@ -0,0 +1,45 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{parse::Parse, parse_quote, visit_mut::VisitMut, ItemFn};
pub(crate) fn expand(_attr: Attrs, mut item_fn: ItemFn) -> TokenStream {
item_fn.attrs.push(parse_quote!(#[tokio::test]));
let nest_service_fn = replace_nest_with_nest_service(item_fn.clone());
quote! {
#item_fn
#nest_service_fn
}
}
pub(crate) struct Attrs;
impl Parse for Attrs {
fn parse(_input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
Ok(Self)
}
}
fn replace_nest_with_nest_service(mut item_fn: ItemFn) -> Option<ItemFn> {
item_fn.sig.ident = format_ident!("{}_with_nest_service", item_fn.sig.ident);
let mut visitor = NestToNestService::default();
syn::visit_mut::visit_item_fn_mut(&mut visitor, &mut item_fn);
(visitor.count > 0).then_some(item_fn)
}
#[derive(Default)]
struct NestToNestService {
count: usize,
}
impl VisitMut for NestToNestService {
fn visit_expr_method_call_mut(&mut self, i: &mut syn::ExprMethodCall) {
if i.method == "nest" && i.args.len() == 2 {
i.method = parse_quote!(nest_service);
self.count += 1;
}
}
}
+891
View File
@@ -0,0 +1,891 @@
use std::{collections::HashSet, fmt};
use crate::{
attr_parsing::{parse_assignment_attribute, second},
with_position::{Position, WithPosition},
};
use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, ReturnType, Token, Type};
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenStream {
let Attrs { state_ty } = attr;
let mut state_ty = state_ty.map(second);
let check_extractor_count = check_extractor_count(&item_fn, kind);
let check_path_extractor = check_path_extractor(&item_fn, kind);
let check_output_tuples = check_output_tuples(&item_fn);
let check_output_impls_into_response = if check_output_tuples.is_empty() {
check_output_impls_into_response(&item_fn)
} else {
check_output_tuples
};
// If the function is generic, we can't reliably check its inputs or whether the future it
// returns is `Send`. Skip those checks to avoid unhelpful additional compiler errors.
let check_inputs_and_future_send = if item_fn.sig.generics.params.is_empty() {
let mut err = None;
if state_ty.is_none() {
let state_types_from_args = state_types_from_args(&item_fn);
#[allow(clippy::comparison_chain)]
if state_types_from_args.len() == 1 {
state_ty = state_types_from_args.into_iter().next();
} else if state_types_from_args.len() > 1 {
err = Some(
syn::Error::new(
Span::call_site(),
format!(
"can't infer state type, please add set it explicitly, as in \
`#[axum_macros::debug_{kind}(state = MyStateType)]`"
),
)
.into_compile_error(),
);
}
}
err.unwrap_or_else(|| {
let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(()));
let check_future_send = check_future_send(&item_fn, kind);
if let Some(check_input_order) = check_input_order(&item_fn, kind) {
quote! {
#check_input_order
#check_future_send
}
} else {
let check_inputs_impls_from_request =
check_inputs_impls_from_request(&item_fn, state_ty, kind);
quote! {
#check_inputs_impls_from_request
#check_future_send
}
}
})
} else {
syn::Error::new_spanned(
&item_fn.sig.generics,
format!("`#[axum_macros::debug_{kind}]` doesn't support generic functions"),
)
.into_compile_error()
};
let middleware_takes_next_as_last_arg =
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn));
quote! {
#item_fn
#check_extractor_count
#check_path_extractor
#check_output_impls_into_response
#check_inputs_and_future_send
#middleware_takes_next_as_last_arg
}
}
#[derive(Clone, Copy)]
pub(crate) enum FunctionKind {
Handler,
Middleware,
}
impl fmt::Display for FunctionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FunctionKind::Handler => f.write_str("handler"),
FunctionKind::Middleware => f.write_str("middleware"),
}
}
}
impl FunctionKind {
fn name_uppercase_plural(&self) -> &'static str {
match self {
FunctionKind::Handler => "Handlers",
FunctionKind::Middleware => "Middleware",
}
}
}
mod kw {
syn::custom_keyword!(body);
syn::custom_keyword!(state);
}
pub(crate) struct Attrs {
state_ty: Option<(kw::state, Type)>,
}
impl Parse for Attrs {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let mut state_ty = None;
while !input.is_empty() {
let lh = input.lookahead1();
if lh.peek(kw::state) {
parse_assignment_attribute(input, &mut state_ty)?;
} else {
return Err(lh.error());
}
let _ = input.parse::<Token![,]>();
}
Ok(Self { state_ty })
}
}
fn check_extractor_count(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
let max_extractors = 16;
let inputs = item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind))
.count();
if inputs <= max_extractors {
None
} else {
let error_message = format!(
"{} cannot take more than {max_extractors} arguments. \
Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors",
kind.name_uppercase_plural(),
);
let error = syn::Error::new_spanned(&item_fn.sig.inputs, error_message).to_compile_error();
Some(error)
}
}
fn extractor_idents(
item_fn: &ItemFn,
kind: FunctionKind,
) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
item_fn
.sig
.inputs
.iter()
.filter(move |arg| skip_next_arg(arg, kind))
.enumerate()
.filter_map(|(idx, fn_arg)| match fn_arg {
FnArg::Receiver(_) => None,
FnArg::Typed(pat_type) => {
if let Type::Path(type_path) = &*pat_type.ty {
type_path
.path
.segments
.last()
.map(|segment| (idx, fn_arg, &segment.ident))
} else {
None
}
}
})
}
fn check_path_extractor(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
let path_extractors = extractor_idents(item_fn, kind)
.filter(|(_, _, ident)| *ident == "Path")
.collect::<Vec<_>>();
if path_extractors.len() > 1 {
path_extractors
.into_iter()
.map(|(_, arg, _)| {
syn::Error::new_spanned(
arg,
"Multiple parameters must be extracted with a tuple \
`Path<(_, _)>` or a struct `Path<YourParams>`, not by applying \
multiple `Path<_>` extractors",
)
.to_compile_error()
})
.collect()
} else {
quote! {}
}
}
fn is_self_pat_type(typed: &syn::PatType) -> bool {
let ident = if let syn::Pat::Ident(ident) = &*typed.pat {
&ident.ident
} else {
return false;
};
ident == "self"
}
fn check_inputs_impls_from_request(
item_fn: &ItemFn,
state_ty: Type,
kind: FunctionKind,
) -> TokenStream {
let takes_self = item_fn.sig.inputs.first().is_some_and(|arg| match arg {
FnArg::Receiver(_) => true,
FnArg::Typed(typed) => is_self_pat_type(typed),
});
WithPosition::new(
item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind)),
)
.enumerate()
.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 {
FnArg::Receiver(receiver) => {
if receiver.reference.is_some() {
return syn::Error::new_spanned(
receiver,
"Handlers must only take owned values",
)
.into_compile_error();
}
let span = receiver.span();
(span, syn::parse_quote!(Self))
}
FnArg::Typed(typed) => {
let ty = &typed.ty;
let span = ty.span();
if is_self_pat_type(typed) {
(span, syn::parse_quote!(Self))
} else {
(span, ty.clone())
}
}
};
let consumes_request = request_consuming_type_name(&ty).is_some();
let check_fn = format_ident!(
"__axum_macros_check_{}_{}_from_request_check",
item_fn.sig.ident,
idx,
span = span,
);
let call_check_fn = format_ident!(
"__axum_macros_check_{}_{}_from_request_call_check",
item_fn.sig.ident,
idx,
span = span,
);
let call_check_fn_body = if takes_self {
quote_spanned! {span=>
Self::#check_fn();
}
} else {
quote_spanned! {span=>
#check_fn();
}
};
let check_fn_generics = if must_impl_from_request_parts || consumes_request {
quote! {}
} else {
quote! { <M> }
};
let from_request_bound = if must_impl_from_request_parts {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequestParts<#state_ty> + Send
}
} else if consumes_request {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequest<#state_ty> + Send
}
} else {
quote_spanned! {span=>
#ty: ::axum::extract::FromRequest<#state_ty, M> + Send
}
};
quote_spanned! {span=>
#[allow(warnings)]
#[doc(hidden)]
fn #check_fn #check_fn_generics()
where
#from_request_bound,
{}
// we have to call the function to actually trigger a compile error
// since the function is generic, just defining it is not enough
#[allow(warnings)]
#[doc(hidden)]
fn #call_check_fn()
{
#call_check_fn_body
}
}
})
.collect::<TokenStream>()
}
fn check_output_tuples(item_fn: &ItemFn) -> TokenStream {
let elems = match &item_fn.sig.output {
ReturnType::Type(_, ty) => match &**ty {
Type::Tuple(tuple) => &tuple.elems,
_ => return quote! {},
},
ReturnType::Default => return quote! {},
};
let handler_ident = &item_fn.sig.ident;
match elems.len() {
0 => quote! {},
n if n > 17 => syn::Error::new_spanned(
&item_fn.sig.output,
"Cannot return tuples with more than 17 elements",
)
.to_compile_error(),
_ => WithPosition::new(elems)
.enumerate()
.map(|(idx, arg)| match arg {
Position::First(ty) => match extract_clean_typename(ty).as_deref() {
Some("StatusCode" | "Response") => quote! {},
Some("Parts") => check_is_response_parts(ty, handler_ident, idx),
Some(_) | None => {
if let Some(tn) = well_known_last_response_type(ty) {
syn::Error::new_spanned(
ty,
format!(
"`{tn}` must be the last element \
in a response tuple"
),
)
.to_compile_error()
} else {
check_into_response_parts(ty, handler_ident, idx)
}
}
},
Position::Middle(ty) => {
if let Some(tn) = well_known_last_response_type(ty) {
syn::Error::new_spanned(
ty,
format!("`{tn}` must be the last element in a response tuple"),
)
.to_compile_error()
} else {
check_into_response_parts(ty, handler_ident, idx)
}
}
Position::Last(ty) | Position::Only(ty) => check_into_response(handler_ident, ty),
})
.collect::<TokenStream>(),
}
}
fn check_into_response(handler: &Ident, ty: &Type) -> TokenStream {
let (span, ty) = (ty.span(), ty.clone());
let check_fn = format_ident!(
"__axum_macros_check_{handler}_into_response_check",
span = span,
);
let call_check_fn = format_ident!(
"__axum_macros_check_{handler}_into_response_call_check",
span = span,
);
let call_check_fn_body = quote_spanned! {span=>
#check_fn();
};
let from_request_bound = quote_spanned! {span=>
#ty: ::axum::response::IntoResponse
};
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #check_fn()
where
#from_request_bound,
{}
// we have to call the function to actually trigger a compile error
// since the function is generic, just defining it is not enough
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #call_check_fn() {
#call_check_fn_body
}
}
}
fn check_is_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStream {
let (span, ty) = (ty.span(), ty.clone());
let check_fn = format_ident!(
"__axum_macros_check_{}_is_response_parts_{index}_check",
ident,
span = span,
);
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #check_fn(parts: #ty) -> ::axum::http::response::Parts {
parts
}
}
}
fn check_into_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStream {
let (span, ty) = (ty.span(), ty.clone());
let check_fn = format_ident!(
"__axum_macros_check_{}_into_response_parts_{index}_check",
ident,
span = span,
);
let call_check_fn = format_ident!(
"__axum_macros_check_{}_into_response_parts_{index}_call_check",
ident,
span = span,
);
let call_check_fn_body = quote_spanned! {span=>
#check_fn();
};
let from_request_bound = quote_spanned! {span=>
#ty: ::axum::response::IntoResponseParts
};
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #check_fn()
where
#from_request_bound,
{}
// we have to call the function to actually trigger a compile error
// since the function is generic, just defining it is not enough
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #call_check_fn() {
#call_check_fn_body
}
}
}
fn check_input_order(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
let number_of_inputs = item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind))
.count();
let types_that_consume_the_request = item_fn
.sig
.inputs
.iter()
.filter(|arg| skip_next_arg(arg, kind))
.enumerate()
.filter_map(|(idx, arg)| {
let ty = match arg {
FnArg::Typed(pat_type) => &*pat_type.ty,
FnArg::Receiver(_) => return None,
};
let type_name = request_consuming_type_name(ty)?;
Some((idx, type_name, ty.span()))
})
.collect::<Vec<_>>();
if types_that_consume_the_request.is_empty() {
return None;
};
// exactly one type that consumes the request
if types_that_consume_the_request.len() == 1 {
// and that is not the last
if types_that_consume_the_request[0].0 != number_of_inputs - 1 {
let (_idx, type_name, span) = &types_that_consume_the_request[0];
let error = format!(
"`{type_name}` consumes the request body and thus must be \
the last argument to the handler function"
);
return Some(quote_spanned! {*span=>
compile_error!(#error);
});
} else {
return None;
}
}
if types_that_consume_the_request.len() == 2 {
let (_, first, _) = &types_that_consume_the_request[0];
let (_, second, _) = &types_that_consume_the_request[1];
let error = format!(
"Can't have two extractors that consume the request body. \
`{first}` and `{second}` both do that.",
);
let span = item_fn.sig.inputs.span();
Some(quote_spanned! {span=>
compile_error!(#error);
})
} else {
let types = WithPosition::new(types_that_consume_the_request)
.map(|pos| match pos {
Position::First((_, type_name, _)) | Position::Middle((_, type_name, _)) => {
format!("`{type_name}`, ")
}
Position::Last((_, type_name, _)) => format!("and `{type_name}`"),
Position::Only(_) => unreachable!(),
})
.collect::<String>();
let error = format!(
"Can't have more than one extractor that consume the request body. \
{types} all do that.",
);
let span = item_fn.sig.inputs.span();
Some(quote_spanned! {span=>
compile_error!(#error);
})
}
}
fn extract_clean_typename(ty: &Type) -> Option<String> {
let path = match ty {
Type::Path(type_path) => &type_path.path,
_ => return None,
};
path.segments.last().map(|p| p.ident.to_string())
}
fn request_consuming_type_name(ty: &Type) -> Option<&'static str> {
let typename = extract_clean_typename(ty)?;
let type_name = match &*typename {
"Json" => "Json<_>",
"RawBody" => "RawBody<_>",
"RawForm" => "RawForm",
"Multipart" => "Multipart",
"Protobuf" => "Protobuf",
"JsonLines" => "JsonLines<_>",
"Form" => "Form<_>",
"Request" => "Request<_>",
"Bytes" => "Bytes",
"String" => "String",
"Parts" => "Parts",
_ => return None,
};
Some(type_name)
}
fn well_known_last_response_type(ty: &Type) -> Option<&'static str> {
let typename = extract_clean_typename(ty)?;
let type_name = match &*typename {
"Json" => "Json<_>",
"Protobuf" => "Protobuf",
"JsonLines" => "JsonLines<_>",
"Form" => "Form<_>",
"Bytes" => "Bytes",
"String" => "String",
_ => return None,
};
Some(type_name)
}
fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
let ty = match &item_fn.sig.output {
syn::ReturnType::Default => return quote! {},
syn::ReturnType::Type(_, ty) => ty,
};
let span = ty.span();
let declare_inputs = item_fn
.sig
.inputs
.iter()
.filter_map(|arg| match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(pat_ty) => {
let pat = &pat_ty.pat;
let ty = &pat_ty.ty;
Some(quote! {
let #pat: #ty = panic!();
})
}
})
.collect::<TokenStream>();
let block = &item_fn.block;
let make_value_name = format_ident!(
"__axum_macros_check_{}_into_response_make_value",
item_fn.sig.ident
);
let make = if item_fn.sig.asyncness.is_some() {
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
async fn #make_value_name() -> #ty {
#declare_inputs
#block
}
}
} else {
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #make_value_name() -> #ty {
#declare_inputs
#block
}
}
};
let name = format_ident!("__axum_macros_check_{}_into_response", item_fn.sig.ident);
if let Some(receiver) = self_receiver(item_fn) {
quote_spanned! {span=>
#make
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
async fn #name() {
fn check<T>(_: T)
where T: ::axum::response::IntoResponse
{}
let value = #receiver #make_value_name().await;
check(value);
}
}
} else {
quote_spanned! {span=>
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
async fn #name() {
#make
fn check<T>(_: T)
where T: ::axum::response::IntoResponse
{}
let value = #make_value_name().await;
check(value);
}
}
}
}
fn check_future_send(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
if item_fn.sig.asyncness.is_none() {
match &item_fn.sig.output {
syn::ReturnType::Default => {
return syn::Error::new_spanned(
item_fn.sig.fn_token,
format!("{} must be `async fn`s", kind.name_uppercase_plural()),
)
.into_compile_error();
}
syn::ReturnType::Type(_, ty) => ty,
};
}
let span = item_fn.sig.ident.span();
let handler_name = &item_fn.sig.ident;
let args = item_fn.sig.inputs.iter().map(|_| {
quote_spanned! {span=> panic!() }
});
let name = format_ident!("__axum_macros_check_{}_future", item_fn.sig.ident);
let define_check = quote! {
fn check<T>(_: T)
where T: ::std::future::Future + Send
{}
};
let do_check = quote! {
check(future);
};
if let Some(receiver) = self_receiver(item_fn) {
quote! {
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #name() {
#define_check
let future = #receiver #handler_name(#(#args),*);
#do_check
}
}
} else {
quote! {
#[allow(warnings)]
#[allow(unreachable_code)]
#[doc(hidden)]
fn #name() {
#item_fn
#define_check
let future = #handler_name(#(#args),*);
#do_check
}
}
}
}
fn self_receiver(item_fn: &ItemFn) -> Option<TokenStream> {
let takes_self = item_fn.sig.inputs.iter().any(|arg| match arg {
FnArg::Receiver(_) => true,
FnArg::Typed(typed) => is_self_pat_type(typed),
});
if takes_self {
return Some(quote! { Self:: });
}
if let syn::ReturnType::Type(_, ty) = &item_fn.sig.output {
if let syn::Type::Path(path) = &**ty {
let segments = &path.path.segments;
if segments.len() == 1 {
if let Some(last) = segments.last() {
match &last.arguments {
syn::PathArguments::None if last.ident == "Self" => {
return Some(quote! { Self:: });
}
_ => {}
}
}
}
}
}
None
}
/// Given a signature like
///
/// ```skip
/// #[debug_handler]
/// async fn handler(
/// _: axum::extract::State<AppState>,
/// _: State<AppState>,
/// ) {}
/// ```
///
/// This will extract `AppState`.
///
/// Returns `None` if there are no `State` args or multiple of different types.
fn state_types_from_args(item_fn: &ItemFn) -> HashSet<Type> {
let types = item_fn
.sig
.inputs
.iter()
.filter_map(|input| match input {
FnArg::Receiver(_) => None,
FnArg::Typed(pat_type) => Some(pat_type),
})
.map(|pat_type| &*pat_type.ty);
crate::infer_state_types(types).collect()
}
fn next_is_last_input(item_fn: &ItemFn) -> TokenStream {
let next_args = item_fn
.sig
.inputs
.iter()
.enumerate()
.filter(|(_, arg)| !skip_next_arg(arg, FunctionKind::Middleware))
.collect::<Vec<_>>();
if next_args.is_empty() {
return quote! {
compile_error!(
"Middleware functions must take `axum::middleware::Next` as the last argument",
);
};
}
if next_args.len() == 1 {
let (idx, arg) = &next_args[0];
if *idx != item_fn.sig.inputs.len() - 1 {
return quote_spanned! {arg.span()=>
compile_error!("`axum::middleware::Next` must the last argument");
};
}
}
if next_args.len() >= 2 {
return quote! {
compile_error!(
"Middleware functions can only take one argument of type `axum::middleware::Next`",
);
};
}
quote! {}
}
fn skip_next_arg(arg: &FnArg, kind: FunctionKind) -> bool {
match kind {
FunctionKind::Handler => true,
FunctionKind::Middleware => match arg {
FnArg::Receiver(_) => true,
FnArg::Typed(pat_type) => {
if let Type::Path(type_path) = &*pat_type.ty {
type_path
.path
.segments
.last()
.map_or(true, |path_segment| path_segment.ident != "Next")
} else {
true
}
}
},
}
}
#[test]
fn ui_debug_handler() {
crate::run_ui_tests("debug_handler");
}
#[test]
fn ui_debug_middleware() {
crate::run_ui_tests("debug_middleware");
}
+105
View File
@@ -0,0 +1,105 @@
use proc_macro2::{Ident, TokenStream};
use quote::quote_spanned;
use syn::{
parse::{Parse, ParseStream},
spanned::Spanned,
Field, ItemStruct, Token, Type,
};
use crate::attr_parsing::{combine_unary_attribute, parse_attrs, Combine};
pub(crate) fn expand(item: ItemStruct) -> syn::Result<TokenStream> {
if !item.generics.params.is_empty() {
return Err(syn::Error::new_spanned(
item.generics,
"`#[derive(FromRef)]` doesn't support generics",
));
}
let tokens = item
.fields
.iter()
.enumerate()
.map(|(idx, field)| expand_field(&item.ident, idx, field))
.collect();
Ok(tokens)
}
fn expand_field(state: &Ident, idx: usize, field: &Field) -> TokenStream {
let FieldAttrs { skip } = match parse_attrs("from_ref", &field.attrs) {
Ok(attrs) => attrs,
Err(err) => return err.into_compile_error(),
};
if skip.is_some() {
return TokenStream::default();
}
let field_ty = &field.ty;
let span = field.ty.span();
let body = if let Some(field_ident) = &field.ident {
if matches!(field_ty, Type::Reference(_)) {
quote_spanned! {span=> state.#field_ident }
} else {
quote_spanned! {span=> state.#field_ident.clone() }
}
} else {
let idx = syn::Index {
index: idx as _,
span: field.span(),
};
quote_spanned! {span=> state.#idx.clone() }
};
quote_spanned! {span=>
#[allow(clippy::clone_on_copy, clippy::clone_on_ref_ptr)]
impl ::axum::extract::FromRef<#state> for #field_ty {
fn from_ref(state: &#state) -> Self {
#body
}
}
}
}
mod kw {
syn::custom_keyword!(skip);
}
#[derive(Default)]
pub(super) struct FieldAttrs {
pub(super) skip: Option<kw::skip>,
}
impl Parse for FieldAttrs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut skip = None;
while !input.is_empty() {
let lh = input.lookahead1();
if lh.peek(kw::skip) {
skip = Some(input.parse()?);
} else {
return Err(lh.error());
}
let _ = input.parse::<Token![,]>();
}
Ok(Self { skip })
}
}
impl Combine for FieldAttrs {
fn combine(mut self, other: Self) -> syn::Result<Self> {
let Self { skip } = other;
combine_unary_attribute(&mut self.skip, skip)?;
Ok(self)
}
}
#[test]
fn ui() {
crate::run_ui_tests("from_ref");
}
@@ -0,0 +1,93 @@
use crate::attr_parsing::{combine_attribute, parse_parenthesized_attribute, Combine};
use syn::{
parse::{Parse, ParseStream},
Token,
};
pub(crate) mod kw {
syn::custom_keyword!(via);
syn::custom_keyword!(rejection);
syn::custom_keyword!(state);
}
#[derive(Default)]
pub(super) struct FromRequestContainerAttrs {
pub(super) via: Option<(kw::via, syn::Path)>,
pub(super) rejection: Option<(kw::rejection, syn::Path)>,
pub(super) state: Option<(kw::state, syn::Type)>,
}
impl Parse for FromRequestContainerAttrs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut via = None;
let mut rejection = None;
let mut state = None;
while !input.is_empty() {
let lh = input.lookahead1();
if lh.peek(kw::via) {
parse_parenthesized_attribute(input, &mut via)?;
} else if lh.peek(kw::rejection) {
parse_parenthesized_attribute(input, &mut rejection)?;
} else if lh.peek(kw::state) {
parse_parenthesized_attribute(input, &mut state)?;
} else {
return Err(lh.error());
}
let _ = input.parse::<Token![,]>();
}
Ok(Self {
via,
rejection,
state,
})
}
}
impl Combine for FromRequestContainerAttrs {
fn combine(mut self, other: Self) -> syn::Result<Self> {
let Self {
via,
rejection,
state,
} = other;
combine_attribute(&mut self.via, via)?;
combine_attribute(&mut self.rejection, rejection)?;
combine_attribute(&mut self.state, state)?;
Ok(self)
}
}
#[derive(Default)]
pub(super) struct FromRequestFieldAttrs {
pub(super) via: Option<(kw::via, syn::Path)>,
}
impl Parse for FromRequestFieldAttrs {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut via = None;
while !input.is_empty() {
let lh = input.lookahead1();
if lh.peek(kw::via) {
parse_parenthesized_attribute(input, &mut via)?;
} else {
return Err(lh.error());
}
let _ = input.parse::<Token![,]>();
}
Ok(Self { via })
}
}
impl Combine for FromRequestFieldAttrs {
fn combine(mut self, other: Self) -> syn::Result<Self> {
let Self { via } = other;
combine_attribute(&mut self.via, via)?;
Ok(self)
}
}
File diff suppressed because it is too large Load Diff
+828
View File
@@ -0,0 +1,828 @@
//! Macros for [`axum`].
//!
//! [`axum`]: https://crates.io/crates/axum
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(test, allow(clippy::float_cmp))]
#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
use debug_handler::FunctionKind;
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse::Parse, Type};
mod attr_parsing;
#[cfg(feature = "__private")]
mod axum_test;
mod debug_handler;
mod from_ref;
mod from_request;
mod typed_path;
mod with_position;
use from_request::Trait::{FromRequest, FromRequestParts};
/// Derive an implementation of [`FromRequest`].
///
/// Supports generating two kinds of implementations:
/// 1. One that extracts each field individually.
/// 2. Another that extracts the whole type at once via another extractor.
///
/// # Each field individually
///
/// By default `#[derive(FromRequest)]` will call `FromRequest::from_request` for each field:
///
/// ```
/// use axum_macros::FromRequest;
/// use axum::{
/// extract::Extension,
/// body::Bytes,
/// };
/// use axum_extra::{
/// TypedHeader,
/// headers::ContentType,
/// };
///
/// #[derive(FromRequest)]
/// struct MyExtractor {
/// state: Extension<State>,
/// content_type: TypedHeader<ContentType>,
/// request_body: Bytes,
/// }
///
/// #[derive(Clone)]
/// struct State {
/// // ...
/// }
///
/// async fn handler(extractor: MyExtractor) {}
/// ```
///
/// This requires that each field is an extractor (i.e. implements [`FromRequest`]).
///
/// Note that only the last field can consume the request body. Therefore this doesn't compile:
///
/// ```compile_fail
/// use axum_macros::FromRequest;
/// use axum::body::Bytes;
///
/// #[derive(FromRequest)]
/// struct MyExtractor {
/// // only the last field can implement `FromRequest`
/// // other fields must only implement `FromRequestParts`
/// bytes: Bytes,
/// string: String,
/// }
/// ```
///
/// ## Extracting via another extractor
///
/// You can use `#[from_request(via(...))]` to extract a field via another extractor, meaning the
/// field itself doesn't need to implement `FromRequest`:
///
/// ```
/// use axum_macros::FromRequest;
/// use axum::{
/// extract::Extension,
/// body::Bytes,
/// };
/// use axum_extra::{
/// TypedHeader,
/// headers::ContentType,
/// };
///
/// #[derive(FromRequest)]
/// struct MyExtractor {
/// // This will extracted via `Extension::<State>::from_request`
/// #[from_request(via(Extension))]
/// state: State,
/// // and this via `TypedHeader::<ContentType>::from_request`
/// #[from_request(via(TypedHeader))]
/// content_type: ContentType,
/// // Can still be combined with other extractors
/// request_body: Bytes,
/// }
///
/// #[derive(Clone)]
/// struct State {
/// // ...
/// }
///
/// async fn handler(extractor: MyExtractor) {}
/// ```
///
/// Note this requires the via extractor to be a generic newtype struct (a tuple struct with
/// exactly one public field) that implements `FromRequest`:
///
/// ```
/// pub struct ViaExtractor<T>(pub T);
///
/// // impl<T, S> FromRequest<S> for ViaExtractor<T> { ... }
/// ```
///
/// More complex via extractors are not supported and require writing a manual implementation.
///
/// ## Optional fields
///
/// `#[from_request(via(...))]` supports `Option<_>` and `Result<_, _>` to make fields optional:
///
/// ```
/// use axum_macros::FromRequest;
/// use axum_extra::{
/// TypedHeader,
/// headers::{ContentType, UserAgent},
/// typed_header::TypedHeaderRejection,
/// };
///
/// #[derive(FromRequest)]
/// struct MyExtractor {
/// // This will extracted via `Option::<TypedHeader<ContentType>>::from_request`
/// #[from_request(via(TypedHeader))]
/// content_type: Option<ContentType>,
/// // This will extracted via
/// // `Result::<TypedHeader<UserAgent>, TypedHeaderRejection>::from_request`
/// #[from_request(via(TypedHeader))]
/// user_agent: Result<UserAgent, TypedHeaderRejection>,
/// }
///
/// async fn handler(extractor: MyExtractor) {}
/// ```
///
/// ## The rejection
///
/// By default [`axum::response::Response`] will be used as the rejection. You can also use your own
/// rejection type with `#[from_request(rejection(YourType))]`:
///
/// ```
/// use axum::{
/// extract::{
/// rejection::{ExtensionRejection, StringRejection},
/// FromRequest,
/// },
/// Extension,
/// response::{Response, IntoResponse},
/// };
///
/// #[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
/// }
/// }
/// ```
///
/// ## Concrete state
///
/// If the extraction can be done only for a concrete state, that type can be specified with
/// `#[from_request(state(YourState))]`:
///
/// ```
/// use axum::extract::{FromRequest, FromRequestParts};
///
/// #[derive(Clone)]
/// struct CustomState;
///
/// struct MyInnerType;
///
/// impl FromRequestParts<CustomState> for MyInnerType {
/// // ...
/// # type Rejection = ();
///
/// # async fn from_request_parts(
/// # _parts: &mut axum::http::request::Parts,
/// # _state: &CustomState
/// # ) -> Result<Self, Self::Rejection> {
/// # todo!()
/// # }
/// }
///
/// #[derive(FromRequest)]
/// #[from_request(state(CustomState))]
/// struct MyExtractor {
/// custom: MyInnerType,
/// body: String,
/// }
/// ```
///
/// This is not needed for a `State<T>` as the type is inferred in that case.
///
/// ```
/// use axum::extract::{FromRequest, FromRequestParts, State};
///
/// #[derive(Clone)]
/// struct CustomState;
///
/// #[derive(FromRequest)]
/// struct MyExtractor {
/// custom: State<CustomState>,
/// body: String,
/// }
/// ```
///
/// # The whole type at once
///
/// By using `#[from_request(via(...))]` on the container you can extract the whole type at once,
/// instead of each field individually:
///
/// ```
/// use axum_macros::FromRequest;
/// use axum::extract::Extension;
///
/// // This will extracted via `Extension::<State>::from_request`
/// #[derive(Clone, FromRequest)]
/// #[from_request(via(Extension))]
/// struct State {
/// // ...
/// }
///
/// async fn handler(state: State) {}
/// ```
///
/// 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 only supported on tuple structs with exactly one field. Thus this doesn't work
///
/// ```compile_fail
/// #[derive(axum_macros::FromRequest)]
/// struct MyExtractor<T> {
/// thing: Option<T>,
/// }
/// ```
///
/// [`FromRequest`]: https://docs.rs/axum/0.8/axum/extract/trait.FromRequest.html
/// [`axum::response::Response`]: https://docs.rs/axum/0.8/axum/response/type.Response.html
/// [`axum::extract::rejection::ExtensionRejection`]: https://docs.rs/axum/0.8/axum/extract/rejection/enum.ExtensionRejection.html
#[proc_macro_derive(FromRequest, attributes(from_request))]
pub fn derive_from_request(item: TokenStream) -> TokenStream {
expand_with(item, |item| from_request::expand(item, FromRequest))
}
/// Derive an implementation of [`FromRequestParts`].
///
/// This works similarly to `#[derive(FromRequest)]` except it uses [`FromRequestParts`]. All the
/// same options are supported.
///
/// # Example
///
/// ```
/// use axum_macros::FromRequestParts;
/// use axum::{
/// extract::Query,
/// };
/// use axum_extra::{
/// TypedHeader,
/// headers::ContentType,
/// };
/// use std::collections::HashMap;
///
/// #[derive(FromRequestParts)]
/// struct MyExtractor {
/// #[from_request(via(Query))]
/// query_params: HashMap<String, String>,
/// content_type: TypedHeader<ContentType>,
/// }
///
/// async fn handler(extractor: MyExtractor) {}
/// ```
///
/// # Cannot extract the body
///
/// [`FromRequestParts`] cannot extract the request body:
///
/// ```compile_fail
/// use axum_macros::FromRequestParts;
///
/// #[derive(FromRequestParts)]
/// struct MyExtractor {
/// body: String,
/// }
/// ```
///
/// Use `#[derive(FromRequest)]` for that.
///
/// [`FromRequestParts`]: https://docs.rs/axum/0.8/axum/extract/trait.FromRequestParts.html
#[proc_macro_derive(FromRequestParts, attributes(from_request))]
pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
expand_with(item, |item| from_request::expand(item, FromRequestParts))
}
/// Generates better error messages when applied to handler functions.
///
/// While using [`axum`], you can get long error messages for simple mistakes. For example:
///
/// ```compile_fail
/// use axum::{routing::get, Router};
///
/// #[tokio::main]
/// async fn main() {
/// let app = Router::new().route("/", get(handler));
///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// }
///
/// fn handler() -> &'static str {
/// "Hello, world"
/// }
/// ```
///
/// You will get a long error message about function not implementing [`Handler`] trait. But why
/// does this function not implement it? To figure it out, the [`debug_handler`] macro can be used.
///
/// ```compile_fail
/// # use axum::{routing::get, Router};
/// # use axum_macros::debug_handler;
/// #
/// # #[tokio::main]
/// # async fn main() {
/// # let app = Router::new().route("/", get(handler));
/// #
/// # let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// # axum::serve(listener, app).await.unwrap();
/// # }
/// #
/// #[debug_handler]
/// fn handler() -> &'static str {
/// "Hello, world"
/// }
/// ```
///
/// ```text
/// error: handlers must be async functions
/// --> main.rs:xx:1
/// |
/// xx | fn handler() -> &'static str {
/// | ^^
/// ```
///
/// As the error message says, handler function needs to be async.
///
/// ```no_run
/// use axum::{routing::get, Router, debug_handler};
///
/// #[tokio::main]
/// async fn main() {
/// let app = Router::new().route("/", get(handler));
///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// }
///
/// #[debug_handler]
/// async fn handler() -> &'static str {
/// "Hello, world"
/// }
/// ```
///
/// # Changing state type
///
/// By default `#[debug_handler]` assumes your state type is `()` unless your handler has a
/// [`axum::extract::State`] argument:
///
/// ```
/// use axum::{debug_handler, extract::State};
///
/// #[debug_handler]
/// async fn handler(
/// // this makes `#[debug_handler]` use `AppState`
/// State(state): State<AppState>,
/// ) {}
///
/// #[derive(Clone)]
/// struct AppState {}
/// ```
///
/// If your handler takes multiple [`axum::extract::State`] arguments or you need to otherwise
/// customize the state type you can set it with `#[debug_handler(state = ...)]`:
///
/// ```
/// use axum::{debug_handler, extract::{State, FromRef}};
///
/// #[debug_handler(state = AppState)]
/// async fn handler(
/// State(app_state): State<AppState>,
/// State(inner_state): State<InnerState>,
/// ) {}
///
/// #[derive(Clone)]
/// struct AppState {
/// inner: InnerState,
/// }
///
/// #[derive(Clone)]
/// struct InnerState {}
///
/// impl FromRef<AppState> for InnerState {
/// fn from_ref(state: &AppState) -> Self {
/// state.inner.clone()
/// }
/// }
/// ```
///
/// # Limitations
///
/// This macro does not work for functions in an `impl` block that don't have a `self` parameter:
///
/// ```compile_fail
/// use axum::{debug_handler, extract::Path};
///
/// struct App {}
///
/// impl App {
/// #[debug_handler]
/// async fn handler(Path(_): Path<String>) {}
/// }
/// ```
///
/// This will yield an error similar to this:
///
/// ```text
/// error[E0425]: cannot find function `__axum_macros_check_handler_0_from_request_check` in this scope
// --> src/main.rs:xx:xx
// |
// xx | pub async fn handler(Path(_): Path<String>) {}
// | ^^^^ not found in this scope
/// ```
///
/// # Performance
///
/// This macro has no effect when compiled with the release profile. (eg. `cargo build --release`)
///
/// [`axum`]: https://docs.rs/axum/0.8
/// [`Handler`]: https://docs.rs/axum/0.8/axum/handler/trait.Handler.html
/// [`axum::extract::State`]: https://docs.rs/axum/0.8/axum/extract/struct.State.html
/// [`debug_handler`]: macro@debug_handler
#[proc_macro_attribute]
pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
#[cfg(not(debug_assertions))]
return input;
#[cfg(debug_assertions)]
return expand_attr_with(_attr, input, |attrs, item_fn| {
debug_handler::expand(attrs, item_fn, FunctionKind::Handler)
});
}
/// Generates better error messages when applied to middleware functions.
///
/// This works similarly to [`#[debug_handler]`](macro@debug_handler) except for middleware using
/// [`axum::middleware::from_fn`].
///
/// # Example
///
/// ```no_run
/// use axum::{
/// routing::get,
/// extract::Request,
/// response::Response,
/// Router,
/// middleware::{self, Next},
/// debug_middleware,
/// };
///
/// #[tokio::main]
/// async fn main() {
/// let app = Router::new()
/// .route("/", get(|| async {}))
/// .layer(middleware::from_fn(my_middleware));
///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// }
///
/// // if this wasn't a valid middleware function #[debug_middleware] would
/// // improve compile error
/// #[debug_middleware]
/// async fn my_middleware(
/// request: Request,
/// next: Next,
/// ) -> Response {
/// next.run(request).await
/// }
/// ```
///
/// # Performance
///
/// This macro has no effect when compiled with the release profile. (eg. `cargo build --release`)
///
/// [`axum`]: https://docs.rs/axum/latest
/// [`axum::middleware::from_fn`]: https://docs.rs/axum/0.8/axum/middleware/fn.from_fn.html
/// [`debug_middleware`]: macro@debug_middleware
#[proc_macro_attribute]
pub fn debug_middleware(_attr: TokenStream, input: TokenStream) -> TokenStream {
#[cfg(not(debug_assertions))]
return input;
#[cfg(debug_assertions)]
return expand_attr_with(_attr, input, |attrs, item_fn| {
debug_handler::expand(attrs, item_fn, FunctionKind::Middleware)
});
}
/// Private API: Do no use this!
///
/// Attribute macro to be placed on test functions that'll generate two functions:
///
/// 1. One identical to the function it was placed on.
/// 2. One where calls to `Router::nest` has been replaced with `Router::nest_service`
///
/// This makes it easy to that `nest` and `nest_service` behaves in the same way, without having to
/// manually write identical tests for both methods.
#[cfg(feature = "__private")]
#[proc_macro_attribute]
#[doc(hidden)]
pub fn __private_axum_test(_attr: TokenStream, input: TokenStream) -> TokenStream {
expand_attr_with(_attr, input, axum_test::expand)
}
/// Derive an implementation of [`axum_extra::routing::TypedPath`].
///
/// See that trait for more details.
///
/// [`axum_extra::routing::TypedPath`]: https://docs.rs/axum-extra/latest/axum_extra/routing/trait.TypedPath.html
#[proc_macro_derive(TypedPath, attributes(typed_path))]
pub fn derive_typed_path(input: TokenStream) -> TokenStream {
expand_with(input, typed_path::expand)
}
/// Derive an implementation of [`FromRef`] for each field in a struct.
///
/// # Example
///
/// ```
/// use axum::{
/// Router,
/// routing::get,
/// extract::{State, FromRef},
/// };
///
/// #
/// # type AuthToken = String;
/// # type DatabasePool = ();
/// #
/// // This will implement `FromRef` for each field in the struct.
/// #[derive(FromRef, Clone)]
/// struct AppState {
/// auth_token: AuthToken,
/// database_pool: DatabasePool,
/// // fields can also be skipped
/// #[from_ref(skip)]
/// api_token: String,
/// }
///
/// // So those types can be extracted via `State`
/// async fn handler(State(auth_token): State<AuthToken>) {}
///
/// async fn other_handler(State(database_pool): State<DatabasePool>) {}
///
/// # let auth_token = Default::default();
/// # let database_pool = Default::default();
/// let state = AppState {
/// auth_token,
/// database_pool,
/// api_token: "secret".to_owned(),
/// };
///
/// let app = Router::new()
/// .route("/", get(handler).post(other_handler))
/// .with_state(state);
/// # let _: axum::Router = app;
/// ```
///
/// [`FromRef`]: https://docs.rs/axum/0.8/axum/extract/trait.FromRef.html
#[proc_macro_derive(FromRef, attributes(from_ref))]
pub fn derive_from_ref(item: TokenStream) -> TokenStream {
expand_with(item, from_ref::expand)
}
fn expand_with<F, I, K>(input: TokenStream, f: F) -> TokenStream
where
F: FnOnce(I) -> syn::Result<K>,
I: Parse,
K: ToTokens,
{
expand(syn::parse(input).and_then(f))
}
fn expand_attr_with<F, A, I, K>(attr: TokenStream, input: TokenStream, f: F) -> TokenStream
where
F: FnOnce(A, I) -> K,
A: Parse,
I: Parse,
K: ToTokens,
{
let expand_result = (|| {
let attr = syn::parse(attr)?;
let input = syn::parse(input)?;
Ok(f(attr, input))
})();
expand(expand_result)
}
fn expand<T>(result: syn::Result<T>) -> TokenStream
where
T: ToTokens,
{
match result {
Ok(tokens) => {
let tokens = (quote! { #tokens }).into();
if std::env::var_os("AXUM_MACROS_DEBUG").is_some() {
eprintln!("{tokens}");
}
tokens
}
Err(err) => err.into_compile_error().into(),
}
}
fn infer_state_types<'a, I>(types: I) -> impl Iterator<Item = Type> + 'a
where
I: Iterator<Item = &'a Type> + 'a,
{
types
.filter_map(|ty| {
if let Type::Path(path) = ty {
Some(&path.path)
} else {
None
}
})
.filter_map(|path| {
if let Some(last_segment) = path.segments.last() {
if last_segment.ident != "State" {
return None;
}
match &last_segment.arguments {
syn::PathArguments::AngleBracketed(args) if args.args.len() == 1 => {
Some(args.args.first().unwrap())
}
_ => None,
}
} else {
None
}
})
.filter_map(|generic_arg| {
if let syn::GenericArgument::Type(ty) = generic_arg {
Some(ty)
} else {
None
}
})
.cloned()
}
#[cfg(test)]
fn run_ui_tests(directory: &str) {
#[rustversion::nightly]
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/{directory}/fail/*.rs"));
t.pass(format!("tests/{directory}/pass/*.rs"));
}
}
#[rustversion::not(nightly)]
fn go(_directory: &str) {}
go(directory);
}
+439
View File
@@ -0,0 +1,439 @@
use proc_macro2::{Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::{parse::Parse, ItemStruct, LitStr, Token};
use crate::attr_parsing::{combine_attribute, parse_parenthesized_attribute, second, Combine};
pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
let ItemStruct {
attrs,
ident,
generics,
fields,
..
} = &item_struct;
if !generics.params.is_empty() || generics.where_clause.is_some() {
return Err(syn::Error::new_spanned(
generics,
"`#[derive(TypedPath)]` doesn't support generics",
));
}
let Attrs { path, rejection } = crate::attr_parsing::parse_attrs("typed_path", attrs)?;
let path = path.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"Missing path: `#[typed_path(\"/foo/bar\")]`",
)
})?;
let rejection = rejection.map(second);
match fields {
syn::Fields::Named(_) => {
let segments = parse_path(&path)?;
Ok(expand_named_fields(ident, path, &segments, rejection))
}
syn::Fields::Unnamed(fields) => {
let segments = parse_path(&path)?;
expand_unnamed_fields(fields, ident, path, &segments, rejection)
}
syn::Fields::Unit => expand_unit_fields(ident, path, rejection),
}
}
mod kw {
syn::custom_keyword!(rejection);
}
#[derive(Default)]
struct Attrs {
path: Option<LitStr>,
rejection: Option<(kw::rejection, syn::Path)>,
}
impl Parse for Attrs {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let mut path = None;
let mut rejection = None;
while !input.is_empty() {
let lh = input.lookahead1();
if lh.peek(LitStr) {
path = Some(input.parse()?);
} else if lh.peek(kw::rejection) {
parse_parenthesized_attribute(input, &mut rejection)?;
} else {
return Err(lh.error());
}
let _ = input.parse::<Token![,]>();
}
Ok(Self { path, rejection })
}
}
impl Combine for Attrs {
fn combine(mut self, other: Self) -> syn::Result<Self> {
let Self { path, rejection } = other;
if let Some(path) = path {
if self.path.is_some() {
return Err(syn::Error::new_spanned(
path,
"path specified more than once",
));
}
self.path = Some(path);
}
combine_attribute(&mut self.rejection, rejection)?;
Ok(self)
}
}
fn expand_named_fields(
ident: &syn::Ident,
path: LitStr,
segments: &[Segment],
rejection: Option<syn::Path>,
) -> TokenStream {
let format_str = format_str_from_path(segments);
let captures = captures_from_path(segments);
let typed_path_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
}
};
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {
#[allow(clippy::unnecessary_to_owned)]
#[allow(clippy::implicit_clone)]
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let Self { #(#captures,)* } = self;
write!(
f,
#format_str,
#(
#captures = ::axum_extra::__private::utf8_percent_encode(
&#captures.to_string(),
::axum_extra::__private::PATH_SEGMENT,
)
),*
)
}
}
};
let rejection_assoc_type = rejection_assoc_type(&rejection);
let map_err_rejection = map_err_rejection(&rejection);
let from_request_impl = quote! {
#[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident
where
S: Send + Sync,
{
type Rejection = #rejection_assoc_type;
async fn from_request_parts(
parts: &mut ::axum::http::request::Parts,
state: &S,
) -> ::std::result::Result<Self, Self::Rejection> {
<::axum::extract::Path<#ident> as ::axum::extract::FromRequestParts<S>>
::from_request_parts(parts, state)
.await
.map(|path| path.0)
#map_err_rejection
}
}
};
quote! {
#typed_path_impl
#display_impl
#from_request_impl
}
}
fn expand_unnamed_fields(
fields: &syn::FieldsUnnamed,
ident: &syn::Ident,
path: LitStr,
segments: &[Segment],
rejection: Option<syn::Path>,
) -> syn::Result<TokenStream> {
let num_captures = segments
.iter()
.filter(|segment| match segment {
Segment::Capture(_, _) => true,
Segment::Static(_) => false,
})
.count();
let num_fields = fields.unnamed.len();
if num_fields != num_captures {
return Err(syn::Error::new_spanned(
fields,
format!(
"Mismatch in number of captures and fields. Path has {} but struct has {}",
simple_pluralize(num_captures, "capture"),
simple_pluralize(num_fields, "field"),
),
));
}
let destructure_self = segments
.iter()
.filter_map(|segment| match segment {
Segment::Capture(capture, _) => Some(capture),
Segment::Static(_) => None,
})
.enumerate()
.map(|(idx, capture)| {
let idx = syn::Index {
index: idx as _,
span: Span::call_site(),
};
let capture = format_ident!("{}", capture, span = path.span());
quote_spanned! {path.span()=>
#idx: #capture,
}
});
let format_str = format_str_from_path(segments);
let captures = captures_from_path(segments);
let typed_path_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
}
};
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {
#[allow(clippy::unnecessary_to_owned)]
#[allow(clippy::implicit_clone)]
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let Self { #(#destructure_self)* } = self;
write!(
f,
#format_str,
#(
#captures = ::axum_extra::__private::utf8_percent_encode(
&#captures.to_string(),
::axum_extra::__private::PATH_SEGMENT,
)
),*
)
}
}
};
let rejection_assoc_type = rejection_assoc_type(&rejection);
let map_err_rejection = map_err_rejection(&rejection);
let from_request_impl = quote! {
#[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident
where
S: Send + Sync,
{
type Rejection = #rejection_assoc_type;
async fn from_request_parts(
parts: &mut ::axum::http::request::Parts,
state: &S,
) -> ::std::result::Result<Self, Self::Rejection> {
::axum::extract::Path::from_request_parts(parts, state)
.await
.map(|path| path.0)
#map_err_rejection
}
}
};
Ok(quote! {
#typed_path_impl
#display_impl
#from_request_impl
})
}
fn simple_pluralize(count: usize, word: &str) -> String {
if count == 1 {
format!("{count} {word}")
} else {
format!("{count} {word}s")
}
}
fn expand_unit_fields(
ident: &syn::Ident,
path: LitStr,
rejection: Option<syn::Path>,
) -> syn::Result<TokenStream> {
for segment in parse_path(&path)? {
match segment {
Segment::Capture(_, span) => {
return Err(syn::Error::new(
span,
"Typed paths for unit structs cannot contain captures",
));
}
Segment::Static(_) => {}
}
}
let typed_path_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::axum_extra::routing::TypedPath for #ident {
const PATH: &'static str = #path;
}
};
let display_impl = quote_spanned! {path.span()=>
#[automatically_derived]
impl ::std::fmt::Display for #ident {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, #path)
}
}
};
let rejection_assoc_type = if let Some(rejection) = &rejection {
quote! { #rejection }
} else {
quote! { ::axum::http::StatusCode }
};
let create_rejection = if let Some(rejection) = &rejection {
quote! {
Err(<#rejection as ::std::default::Default>::default())
}
} else {
quote! {
Err(::axum::http::StatusCode::NOT_FOUND)
}
};
let from_request_impl = quote! {
#[automatically_derived]
impl<S> ::axum::extract::FromRequestParts<S> for #ident
where
S: Send + Sync,
{
type Rejection = #rejection_assoc_type;
async fn from_request_parts(
parts: &mut ::axum::http::request::Parts,
_state: &S,
) -> ::std::result::Result<Self, Self::Rejection> {
if parts.uri.path() == <Self as ::axum_extra::routing::TypedPath>::PATH {
Ok(Self)
} else {
#create_rejection
}
}
}
};
Ok(quote! {
#typed_path_impl
#display_impl
#from_request_impl
})
}
fn format_str_from_path(segments: &[Segment]) -> String {
segments
.iter()
.map(|segment| match segment {
Segment::Capture(capture, _) => format!("{{{capture}}}"),
Segment::Static(segment) => segment.to_owned(),
})
.collect::<Vec<_>>()
.join("/")
}
fn captures_from_path(segments: &[Segment]) -> Vec<syn::Ident> {
segments
.iter()
.filter_map(|segment| match segment {
Segment::Capture(capture, span) => Some(format_ident!("{}", capture, span = *span)),
Segment::Static(_) => None,
})
.collect::<Vec<_>>()
}
fn parse_path(path: &LitStr) -> syn::Result<Vec<Segment>> {
let value = path.value();
if value.is_empty() {
return Err(syn::Error::new_spanned(
path,
"paths must start with a `/`. Use \"/\" for root routes",
));
} else if !path.value().starts_with('/') {
return Err(syn::Error::new_spanned(path, "paths must start with a `/`"));
}
path.value()
.split('/')
.map(|segment| {
if let Some(capture) = segment
.strip_prefix('{')
.and_then(|segment| segment.strip_suffix('}'))
.and_then(|segment| {
(!segment.starts_with('{') && !segment.ends_with('}')).then_some(segment)
})
.map(|capture| capture.strip_prefix('*').unwrap_or(capture))
{
Ok(Segment::Capture(capture.to_owned(), path.span()))
} else {
Ok(Segment::Static(segment.to_owned()))
}
})
.collect()
}
enum Segment {
Capture(String, Span),
Static(String),
}
fn path_rejection() -> TokenStream {
quote! {
<::axum::extract::Path<Self> as ::axum::extract::FromRequestParts<S>>::Rejection
}
}
fn rejection_assoc_type(rejection: &Option<syn::Path>) -> TokenStream {
match rejection {
Some(rejection) => quote! { #rejection },
None => path_rejection(),
}
}
fn map_err_rejection(rejection: &Option<syn::Path>) -> TokenStream {
rejection
.as_ref()
.map(|rejection| {
let path_rejection = path_rejection();
quote! {
.map_err(|rejection| {
<#rejection as ::std::convert::From<#path_rejection>>::from(rejection)
})
}
})
.unwrap_or_default()
}
#[test]
fn ui() {
crate::run_ui_tests("typed_path");
}
+116
View 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: impl IntoIterator<IntoIter = I>) -> WithPosition<I> {
WithPosition {
handled_first: false,
peekable: iter.into_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> {}