Allow #[serde(serde_path = "...")] to override extern crate serde

This is intended to be used by other crates which provide their own proc
macros and use serde internally. Today there's no consistent way to put
`#[derive(Deserialize)]` on a struct that consistently works, since
crates may be using either `features = ["derive"]` or relying on
`serde_derive` separately.

Even if we assume that everyone is using `features = ["derive"]`,
without this commit, any crate which generates
`#[derive(serde::Deserialize)]` forces its consumers to put `serde` in
their `Cargo.toml`, even if they aren't otherwise using serde for
anything.

Examples of crates which suffer from this in the real world are
tower-web and swirl.

With this feature, it's expected that these crates would have `pub
extern crate serde;` in some accessible path, and add
`#[serde(serde_path = "that_crate::wherever::serde")]` anywhere they
place serde's derives. Those crates would also have to derive
`that_crate::whatever::serde::Deserialize`, or `use` the macros
explicitly beforehand.

The test for this is a little funky, as it's testing this in a way that
is not the intended use case, or even one we want to support. It has its
own module which re-exports all of serde, but defines its own
`Serialize` and `Deserialize` traits. We then test that we generated
impls for those traits, instead of serde's. The only other way to test
this would be to create a new test crate which does not depend on serde,
but instead depends on `serde_derive` and a third crate which publicly
re-exports serde. This feels like way too much overhead for a single
test case, hence the funky test given.

I didn't see anywhere in this repo to document this attribute, so I
assume the docs will have to be done as a separate PR to a separate
repo.

Fixes #1487
This commit is contained in:
Sean Griffin 2019-03-18 15:06:56 -06:00
parent 295730ba1e
commit a295c38ba3
5 changed files with 67 additions and 6 deletions

View File

@ -60,7 +60,7 @@ pub fn expand_derive_deserialize(input: &syn::DeriveInput) -> Result<TokenStream
}
};
Ok(dummy::wrap_in_const("DESERIALIZE", ident, impl_block))
Ok(dummy::wrap_in_const(cont.attrs.serde_path(), "DESERIALIZE", ident, impl_block))
}
fn precondition(cx: &Ctxt, cont: &Container) {

View File

@ -2,7 +2,7 @@ use proc_macro2::{Ident, Span, TokenStream};
use try;
pub fn wrap_in_const(trait_: &str, ty: &Ident, code: TokenStream) -> TokenStream {
pub fn wrap_in_const(serde_path: Option<&syn::Path>, trait_: &str, ty: &Ident, code: TokenStream) -> TokenStream {
let try_replacement = try::replacement();
let dummy_const = Ident::new(
@ -10,13 +10,21 @@ pub fn wrap_in_const(trait_: &str, ty: &Ident, code: TokenStream) -> TokenStream
Span::call_site(),
);
quote! {
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const #dummy_const: () = {
let use_serde = serde_path.map(|path| {
quote!(use #path as _serde;)
}).unwrap_or_else(|| {
quote! {
#[allow(unknown_lints)]
#[cfg_attr(feature = "cargo-clippy", allow(useless_attribute))]
#[allow(rust_2018_idioms)]
extern crate serde as _serde;
}
});
quote! {
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
const #dummy_const: () = {
#use_serde
#try_replacement
#code
};

View File

@ -218,6 +218,7 @@ pub struct Container {
remote: Option<syn::Path>,
identifier: Identifier,
has_flatten: bool,
serde_path: Option<syn::Path>,
}
/// Styles of representing an enum.
@ -298,6 +299,7 @@ impl Container {
let mut remote = Attr::none(cx, "remote");
let mut field_identifier = BoolAttr::none(cx, "field_identifier");
let mut variant_identifier = BoolAttr::none(cx, "variant_identifier");
let mut serde_path = Attr::none(cx, "serde_path");
for meta_items in item.attrs.iter().filter_map(get_serde_meta_items) {
for meta_item in meta_items {
@ -582,6 +584,13 @@ impl Container {
variant_identifier.set_true(word);
}
// Parse `#[serde(serde_path = "foo")]`
Meta(NameValue(ref m)) if m.ident == "serde_path" => {
if let Ok(path) = parse_lit_into_path(cx, &m.ident, &m.lit) {
serde_path.set(&m.ident, path)
}
}
Meta(ref meta_item) => {
cx.error_spanned_by(
meta_item.name(),
@ -613,6 +622,7 @@ impl Container {
remote: remote.get(),
identifier: decide_identifier(cx, item, field_identifier, variant_identifier),
has_flatten: false,
serde_path: serde_path.get(),
}
}
@ -671,6 +681,10 @@ impl Container {
pub fn mark_has_flatten(&mut self) {
self.has_flatten = true;
}
pub fn serde_path(&self) -> Option<&syn::Path> {
self.serde_path.as_ref()
}
}
fn decide_tag(

View File

@ -51,7 +51,7 @@ pub fn expand_derive_serialize(input: &syn::DeriveInput) -> Result<TokenStream,
}
};
Ok(dummy::wrap_in_const("SERIALIZE", ident, impl_block))
Ok(dummy::wrap_in_const(cont.attrs.serde_path(), "SERIALIZE", ident, impl_block))
}
fn precondition(cx: &Ctxt, cont: &Container) {

View File

@ -0,0 +1,39 @@
#[test]
fn test_gen_custom_serde() {
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(serde_path = "fake_serde")]
struct Foo;
// Would be overlapping if serde::Serialize were implemented
impl AssertNotSerdeSerialize for Foo {}
// Would be overlapping if serde::Deserialize were implemented
impl<'a> AssertNotSerdeDeserialize<'a> for Foo {}
fake_serde::assert::<Foo>();
}
mod fake_serde {
pub use serde::*;
pub fn assert<T>()
where
T: Serialize,
T: for<'a> Deserialize<'a>,
{}
pub trait Serialize {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>;
}
pub trait Deserialize<'a>: Sized {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error>;
}
}
trait AssertNotSerdeSerialize {}
impl<T: serde::Serialize> AssertNotSerdeSerialize for T {}
trait AssertNotSerdeDeserialize<'a> {}
impl<'a, T: serde::Deserialize<'a>> AssertNotSerdeDeserialize<'a> for T {}