Automatically discover 'Responder' generic bounds.

This commit presents and applies a new technique for bounding type
generics in derives. In short, for a generic `T` used in a field type of
`Field<T>`, where an eventual bound of `Responder` required, the derive
generates a bound of `Field<T>: Responder`. This removes the need for
any manually provided bounds while simultaneously allowing more
structures to typecheck. For example, generics in header components are
now fully supported.
This commit is contained in:
Sergio Benitez 2021-06-29 03:20:40 -07:00
parent 7c8c06522c
commit 2727d7bb7b
8 changed files with 74 additions and 224 deletions

View File

@ -1,17 +1,13 @@
use quote::ToTokens;
use devise::{*, ext::{TypeExt, SpanDiagnosticExt, GenericsExt}};
use devise::{*, ext::{TypeExt, SpanDiagnosticExt}};
use proc_macro2::TokenStream;
use syn::punctuated::Punctuated;
use syn::parse::Parser;
use crate::exports::*;
use crate::syn_ext::{TypeExt as _, GenericsExt as _};
use crate::http_codegen::{ContentType, Status};
type WherePredicates = Punctuated<syn::WherePredicate, syn::Token![,]>;
#[derive(Debug, Default, FromMeta)]
struct ItemAttr {
bound: Option<SpanWrapped<String>>,
content_type: Option<SpanWrapped<ContentType>>,
status: Option<SpanWrapped<Status>>,
}
@ -21,28 +17,45 @@ struct FieldAttr {
ignore: bool,
}
fn generic_bounds_tokens(input: Input<'_>) -> Result<TokenStream> {
MapperBuild::new()
.try_enum_map(|m, e| mapper::enum_null(m, e))
.try_fields_map(|_, fields| {
let generic_idents = fields.parent.input().generics().type_idents();
let lifetime = |ty: &syn::Type| syn::Lifetime::new("'o", ty.span());
let mut types = fields.iter()
.map(|f| (f, &f.field.inner.ty))
.map(|(f, ty)| (f, ty.with_replaced_lifetimes(lifetime(ty))));
let mut bounds = vec![];
if let Some((_, ty)) = types.next() {
if !ty.is_concrete(&generic_idents) {
bounds.push(quote_spanned!(ty.span() => #ty: #_response::Responder<'r, 'o>));
}
}
for (f, ty) in types {
let attr = FieldAttr::one_from_attrs("response", &f.attrs)?.unwrap_or_default();
if ty.is_concrete(&generic_idents) || attr.ignore {
continue;
}
bounds.push(quote_spanned! { ty.span() =>
#ty: ::std::convert::Into<#_http::Header<'o>>
});
}
Ok(quote!(#(#bounds,)*))
})
.map_input(input)
}
pub fn derive_responder(input: proc_macro::TokenStream) -> TokenStream {
let impl_tokens = quote!(impl<'r, 'o: 'r> ::rocket::response::Responder<'r, 'o>);
let impl_tokens = quote!(impl<'r, 'o: 'r> #_response::Responder<'r, 'o>);
DeriveGenerator::build_for(input, impl_tokens)
.support(Support::Struct | Support::Enum | Support::Lifetime | Support::Type)
.replace_generic(1, 0)
.type_bound_mapper(MapperBuild::new()
.try_input_map(|_, input| {
ItemAttr::one_from_attrs("response", input.attrs())?
.and_then(|attr| attr.bound)
.map(|bound| {
let span = bound.span;
let bounds = WherePredicates::parse_terminated.parse_str(&bound)
.map_err(|e| span.error(format!("invalid bound syntax: {}", e)))?;
Ok(quote_respanned!(span => #bounds))
})
.unwrap_or_else(|| {
let bound = quote!(::rocket::response::Responder<'r, 'o>);
let preds = input.generics().parsed_bounded_types(bound)?;
Ok(quote!(#preds))
})
})
)
.type_bound_mapper(MapperBuild::new().try_input_map(|_, i| generic_bounds_tokens(i)))
.validator(ValidatorBuild::new()
.input_validate(|_, i| match i.generics().lifetimes().count() > 1 {
true => Err(i.generics().span().error("only one lifetime is supported")),
@ -70,7 +83,7 @@ pub fn derive_responder(input: proc_macro::TokenStream) -> TokenStream {
let responder = fields.iter().next().map(|f| {
let (accessor, ty) = (f.accessor(), f.ty.with_stripped_lifetimes());
quote_spanned! { f.span().into() =>
let mut __res = <#ty as ::rocket::response::Responder>::respond_to(
let mut __res = <#ty as #_response::Responder>::respond_to(
#accessor, __req
)?;
}

View File

@ -796,9 +796,15 @@ pub fn derive_from_form(input: TokenStream) -> TokenStream {
/// # Generics
///
/// The derive accepts any number of type generics and at most one lifetime
/// generic. If a type generic is present, the generated implementation will
/// require a bound of `Responder<'r, 'o>` for each generic unless a
/// `#[response(bound = ...)]` attribute as used:
/// generic. If a type generic is present and the generic is used in the first
/// field of a structure, the generated implementation will require a bound of
/// `Responder<'r, 'o>` for the field type containing the generic. In all other
/// fields, unless ignores, a bound of `Into<Header<'o>` is added.
///
/// For example, for a struct `struct Foo<T, H>(Json<T>, H)`, the derive adds:
///
/// * `Json<T>: Responder<'r, 'o>`
/// * `H: Into<Header<'o>>`
///
/// ```rust
/// # #[macro_use] extern crate rocket;
@ -807,21 +813,17 @@ pub fn derive_from_form(input: TokenStream) -> TokenStream {
/// use rocket::http::ContentType;
/// use rocket::response::Responder;
///
/// // The bound `T: Responder` will be added to the generated implementation.
/// // The bound `T: Responder` will be added.
/// #[derive(Responder)]
/// #[response(status = 404, content_type = "html")]
/// struct NotFoundHtml<T>(T);
///
/// // The bound `T: Serialize` will be added to the generated implementation.
/// // This would fail to compile otherwise.
/// // The bound `Json<T>: Responder` will be added.
/// #[derive(Responder)]
/// #[response(bound = "T: Serialize", status = 404)]
/// struct NotFoundJson<T>(Json<T>);
///
/// // The bounds `T: Serialize, E: Responder` will be added to the generated
/// // implementation. This would fail to compile otherwise.
/// // The bounds `Json<T>: Responder, E: Responder` will be added.
/// #[derive(Responder)]
/// #[response(bound = "T: Serialize, E: Responder<'r, 'o>")]
/// enum MyResult<T, E> {
/// Ok(Json<T>),
/// #[response(status = 404)]
@ -829,7 +831,7 @@ pub fn derive_from_form(input: TokenStream) -> TokenStream {
/// }
/// ```
///
/// If a lifetime generic is present, it will be replace with `'o` in the
/// If a lifetime generic is present, it will be replaced with `'o` in the
/// generated implementation `impl Responder<'r, 'o>`:
///
/// ```rust

View File

@ -30,6 +30,16 @@ pub trait FnArgExt {
fn wild(&self) -> Option<&syn::PatWild>;
}
pub trait TypeExt {
fn unfold(&self) -> Vec<Child<'_>>;
fn unfold_with_ty_macros(&self, names: &[&str], mapper: MacTyMapFn) -> Vec<Child<'_>>;
fn is_concrete(&self, generic_ident: &[&Ident]) -> bool;
}
pub trait GenericsExt {
fn type_idents(&self) -> Vec<&Ident>;
}
#[derive(Debug)]
pub struct Child<'a> {
pub parent: Option<Cow<'a, syn::Type>>,
@ -57,12 +67,6 @@ impl IntoOwned for Child<'_> {
type MacTyMapFn = fn(&TokenStream) -> Option<syn::Type>;
pub trait TypeExt {
fn unfold(&self) -> Vec<Child<'_>>;
fn unfold_with_ty_macros(&self, names: &[&str], mapper: MacTyMapFn) -> Vec<Child<'_>>;
fn is_concrete(&self, generic_ident: &[&Ident]) -> bool;
}
impl IdentExt for syn::Ident {
fn prepend(&self, string: &str) -> syn::Ident {
syn::Ident::new(&format!("{}{}", string, self.unraw()), self.span())
@ -229,6 +233,12 @@ impl TypeExt for syn::Type {
}
}
impl GenericsExt for syn::Generics {
fn type_idents(&self) -> Vec<&Ident> {
self.type_params().map(|p| &p.ident).collect()
}
}
#[cfg(test)]
mod tests {
#[test]

View File

@ -110,14 +110,13 @@ async fn responder_baz() {
use rocket::serde::json::Json;
// The bounds `T: Serialize, E: Responder` will be added to the generated
// The bounds `Json<T>: Responder, E: Responder` will be added to the generated
// implementation. This would fail to compile otherwise.
#[derive(Responder)]
#[response(bound = "T: rocket::serde::Serialize, E: Responder<'r, 'o>")]
enum MyResult<'a, T, E> {
enum MyResult<'a, T, E, H> {
Ok(Json<T>),
#[response(status = 404)]
Err(E, ContentType),
Err(E, H),
#[response(status = 500)]
Other(&'a str),
}
@ -128,19 +127,19 @@ async fn generic_responder() {
let local_req = client.get("/");
let req = local_req.inner();
let v: MyResult<_, ()> = MyResult::Ok(Json("hi"));
let v: MyResult<_, (), ContentType> = MyResult::Ok(Json("hi"));
let mut r = v.respond_to(req).unwrap();
assert_eq!(r.status(), Status::Ok);
assert_eq!(r.content_type().unwrap(), ContentType::JSON);
assert_eq!(r.body_mut().to_string().await.unwrap(), "\"hi\"");
let v: MyResult<(), &[u8]> = MyResult::Err(&[7, 13, 23], ContentType::JPEG);
let v: MyResult<(), &[u8], _> = MyResult::Err(&[7, 13, 23], ContentType::JPEG);
let mut r = v.respond_to(req).unwrap();
assert_eq!(r.status(), Status::NotFound);
assert_eq!(r.content_type().unwrap(), ContentType::JPEG);
assert_eq!(r.body_mut().to_bytes().await.unwrap(), vec![7, 13, 23]);
let v: MyResult<(), &[u8]> = MyResult::Other("beep beep");
let v: MyResult<(), &[u8], ContentType> = MyResult::Other("beep beep");
let mut r = v.respond_to(req).unwrap();
assert_eq!(r.status(), Status::InternalServerError);
assert_eq!(r.content_type().unwrap(), ContentType::Text);

View File

@ -140,78 +140,3 @@ note: error occurred while deriving `Responder`
48 | #[derive(Responder)]
| ^^^^^^^^^
= note: this error originates in the derive macro `Responder` (in Nightly builds, run with -Z macro-backtrace for more info)
error: invalid value: expected string literal
--> $DIR/responder.rs:53:20
|
53 | #[response(bound = 10)]
| ^^
|
note: error occurred while deriving `Responder`
--> $DIR/responder.rs:52:10
|
52 | #[derive(Responder)]
| ^^^^^^^^^
= note: this error originates in the derive macro `Responder` (in Nightly builds, run with -Z macro-backtrace for more info)
error: invalid bound syntax: expected `:`
--> $DIR/responder.rs:65:20
|
65 | #[response(bound = "ponies are cool")]
| ^^^^^^^^^^^^^^^^^
|
note: error occurred while deriving `Responder`
--> $DIR/responder.rs:64:10
|
64 | #[derive(Responder)]
| ^^^^^^^^^
= note: this error originates in the derive macro `Responder` (in Nightly builds, run with -Z macro-backtrace for more info)
error: invalid bound syntax: expected `,`
--> $DIR/responder.rs:69:20
|
69 | #[response(bound = "T: ROCKETS + ARE COOLER")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: error occurred while deriving `Responder`
--> $DIR/responder.rs:68:10
|
68 | #[derive(Responder)]
| ^^^^^^^^^
= note: this error originates in the derive macro `Responder` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `Header<'_>: From<E>` is not satisfied
--> $DIR/responder.rs:22:24
|
22 | struct Thing6<T, E>(T, E); // NO ERROR
| ^ the trait `From<E>` is not implemented for `Header<'_>`
|
= note: required because of the requirements on the impl of `Into<Header<'_>>` for `E`
help: consider extending the `where` bound, but there might be an alternative better way to express this requirement
|
21 | #[derive(Responder, Header<'_>: From<E>)]
| ^^^^^^^^^^^^^^^^^^^^^
error[E0277]: the trait bound `T: Responder<'_, '_>` is not satisfied
--> $DIR/responder.rs:58:19
|
58 | struct Thing15<T>(T);
| ^ the trait `Responder<'_, '_>` is not implemented for `T`
|
= note: required by `respond_to`
help: consider further restricting this bound
|
57 | #[response(bound = "T: std::fmt::Display" + rocket::response::Responder<'_, '_>)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0277]: the trait bound `T: Responder<'_, '_>` is not satisfied
--> $DIR/responder.rs:62:19
|
62 | struct Thing16<T>(T);
| ^ the trait `Responder<'_, '_>` is not implemented for `T`
|
= note: required by `respond_to`
help: consider further restricting this bound
|
61 | #[response(bound = "T: std::fmt::Display" + rocket::response::Responder<'_, '_>)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -151,81 +151,3 @@ error: [note] error occurred while deriving `Responder`
| ^^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: invalid value: expected string literal
--> $DIR/responder.rs:53:20
|
53 | #[response(bound = 10)]
| ^^
error: [note] error occurred while deriving `Responder`
--> $DIR/responder.rs:52:10
|
52 | #[derive(Responder)]
| ^^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: invalid bound syntax: expected `:`
--> $DIR/responder.rs:65:20
|
65 | #[response(bound = "ponies are cool")]
| ^^^^^^^^^^^^^^^^^
error: [note] error occurred while deriving `Responder`
--> $DIR/responder.rs:64:10
|
64 | #[derive(Responder)]
| ^^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error: invalid bound syntax: expected `,`
--> $DIR/responder.rs:69:20
|
69 | #[response(bound = "T: ROCKETS + ARE COOLER")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^
error: [note] error occurred while deriving `Responder`
--> $DIR/responder.rs:68:10
|
68 | #[derive(Responder)]
| ^^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `Header<'_>: From<E>` is not satisfied
--> $DIR/responder.rs:22:24
|
22 | struct Thing6<T, E>(T, E); // NO ERROR
| ^ the trait `From<E>` is not implemented for `Header<'_>`
|
= note: required because of the requirements on the impl of `Into<Header<'_>>` for `E`
help: consider extending the `where` bound, but there might be an alternative better way to express this requirement
|
21 | #[derive(Responder, Header<'_>: From<E>)]
| ^^^^^^^^^^^^^^^^^^^^^
error[E0277]: the trait bound `T: Responder<'_, '_>` is not satisfied
--> $DIR/responder.rs:58:19
|
58 | struct Thing15<T>(T);
| ^ the trait `Responder<'_, '_>` is not implemented for `T`
|
= note: required by `respond_to`
help: consider further restricting this bound
|
57 | #[response(bound = "T: std::fmt::Display" + rocket::response::Responder<'_, '_>)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0277]: the trait bound `T: Responder<'_, '_>` is not satisfied
--> $DIR/responder.rs:62:19
|
62 | struct Thing16<T>(T);
| ^ the trait `Responder<'_, '_>` is not implemented for `T`
|
= note: required by `respond_to`
help: consider further restricting this bound
|
61 | #[response(bound = "T: std::fmt::Display" + rocket::response::Responder<'_, '_>)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -19,7 +19,7 @@ struct Thing4<'a, 'b>(&'a str, &'b str);
struct Thing5<T>(T); // NO ERROR
#[derive(Responder)]
struct Thing6<T, E>(T, E); // NO ERROR
struct Thing6<T, E>(T, E);
#[derive(Responder)]
#[response(content_type = "")]
@ -49,24 +49,4 @@ struct Thing12(());
#[response(status = 404, content_type = 120)]
struct Thing13(());
#[derive(Responder)]
#[response(bound = 10)]
struct Thing14(());
#[derive(Responder)]
#[response(bound = "T: std::fmt::Display")]
struct Thing15<T>(T);
#[derive(Responder)]
#[response(bound = "T: std::fmt::Display")]
struct Thing16<T>(T);
#[derive(Responder)]
#[response(bound = "ponies are cool")]
struct Thing17<T>(T);
#[derive(Responder)]
#[response(bound = "T: ROCKETS + ARE COOLER")]
struct Thing18<T>(T);
fn main() {}

View File

@ -35,7 +35,6 @@ use crate::request::Request;
/// use rocket::serde::{Serialize, json::Json};
///
/// #[derive(Responder)]
/// #[response(bound = "T: Serialize")]
/// enum Error<T> {
/// #[response(status = 400)]
/// Unauthorized(Json<T>),