Implement serialization for unit variants and named variants

This commit is contained in:
Aymeric Beringer
2019-10-11 16:32:09 +02:00
commit 353d0c07d4
6 changed files with 389 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target
**/*.rs.bk
Cargo.lock

18
Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "miniserde-enum"
version = "0.1.0"
authors = ["Aymeric Beringer <aymeric.beringer@epita.fr>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dev-dependencies]
miniserde = "0.1"
[dependencies]
syn = { version = "1.0", features = ["derive", "extra-traits"] }
quote = "1.0"
proc-macro2 = "1.0"
miniserde = "0.1" # TODO: remove

97
src/attr.rs Normal file
View File

@@ -0,0 +1,97 @@
use syn::{Attribute, Error, Field, Fields, Lit, Meta, NestedMeta, Result, Variant, DataEnum};
use crate::TagType;
pub(crate) fn tag_type(attrs: &[Attribute], enumeration: &DataEnum) -> Result<TagType> {
let mut tag_type = None;
for attr in attrs {
if !attr.path.is_ident("serde") {
continue;
}
let list = match attr.parse_meta()? {
Meta::List(list) => list,
other => return Err(Error::new_spanned(other, "unsupported attribute")),
};
for meta in &list.nested {
match meta {
NestedMeta::Meta(Meta::NameValue(value)) => {
if value.path.is_ident("tag") {
if let Lit::Str(s) = &value.lit {
if tag_type.is_some() {
return Err(Error::new_spanned(meta, "duplicate tag attribute"));
}
for fields in enumeration.variants.iter().map(|v| &v.fields) {
if let Fields::Unnamed(_) = fields {
return Err(Error::new_spanned(meta,
"enums containing tuple variants cannot be internally tagged"));
}
}
tag_type = Some(TagType::Internal(s.value()));
continue;
}
}
}
NestedMeta::Meta(Meta::Path(path)) => {
if path.is_ident("untagged") {
if tag_type.is_some() {
return Err(Error::new_spanned(meta, "duplicate tag attribute"));
}
tag_type = Some(TagType::Untagged);
continue;
}
}
_ => (),
}
return Err(Error::new_spanned(meta, "unsupported attribute"));
}
}
Ok(tag_type.unwrap_or(TagType::External))
}
/// Find the value of a #[serde(rename = "...")] attribute.
fn attr_rename(attrs: &[Attribute]) -> Result<Option<String>> {
let mut rename = None;
for attr in attrs {
if !attr.path.is_ident("serde") {
continue;
}
let list = match attr.parse_meta()? {
Meta::List(list) => list,
other => return Err(Error::new_spanned(other, "unsupported attribute")),
};
for meta in &list.nested {
if let NestedMeta::Meta(Meta::NameValue(value)) = meta {
if value.path.is_ident("rename") {
if let Lit::Str(s) = &value.lit {
if rename.is_some() {
return Err(Error::new_spanned(meta, "duplicate rename attribute"));
}
rename = Some(s.value());
continue;
}
}
}
return Err(Error::new_spanned(meta, "unsupported attribute"));
}
}
Ok(rename)
}
/// Determine the name of a field, respecting a rename attribute.
pub fn name_of_field(field: &Field) -> Result<String> {
let rename = attr_rename(&field.attrs)?;
Ok(rename.unwrap_or_else(|| field.ident.as_ref().unwrap().to_string()))
}
/// Determine the name of a variant, respecting a rename attribute.
pub fn name_of_variant(var: &Variant) -> Result<String> {
let rename = attr_rename(&var.attrs)?;
Ok(rename.unwrap_or_else(|| var.ident.to_string()))
}

51
src/lib.rs Normal file
View File

@@ -0,0 +1,51 @@
extern crate proc_macro;
mod ser;
pub(crate) mod attr;
use syn::{
parse_macro_input, DeriveInput, Error, Data, Ident, Result,
};
use std::convert::From;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
#[derive(Debug)]
pub(crate) struct Variant {
ident: Ident,
name: Option<String>,
}
impl From<&syn::Variant> for Variant {
fn from(v: &syn::Variant) -> Self {
let ident = v.ident.clone();
Variant {
ident,
name: None,
}
}
}
#[derive(Debug)]
enum TagType {
External,
Internal(String),
Untagged,
}
#[proc_macro_derive(Serialize_enum, attributes(serde))]
pub fn derive_serialize(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(tokens as DeriveInput);
let en = match &input.data {
Data::Enum(en) => en,
_ => return Error::new_spanned(input, "Serialize_enum can only be applied to enums")
.to_compile_error().into()
};
ser::derive(&input, &en).unwrap_or_else(|err| err.to_compile_error()).into()
}

168
src/ser.rs Normal file
View File

@@ -0,0 +1,168 @@
use proc_macro2::{TokenStream, Span};
use syn::{
DeriveInput, Result, DataEnum, Error, Fields, FieldsNamed,
};
use quote::quote;
use crate::{Variant, TagType};
use crate::attr;
pub fn derive(input: &DeriveInput, enumeration: &DataEnum) -> Result<TokenStream> {
if input.generics.lt_token.is_some() || input.generics.where_clause.is_some() {
return Err(Error::new(
Span::call_site(),
"Enums with generics are not supported",
));
}
let ident = &input.ident;
let variants: Vec<Variant> = enumeration.variants.iter().map(Into::into).collect();
let tag_type = attr::tag_type(&input.attrs, &enumeration)?;
let names = enumeration.variants
.iter()
.map(attr::name_of_variant)
.collect::<Result<Vec<_>>>()?;
let begin = enumeration.variants
.iter()
.zip(names.iter())
.map(|(variant, name)| {
let var_ident = &variant.ident;
Ok(match &variant.fields {
Fields::Unit => {
let implementation = serialize_unit(name, &tag_type)?;
quote!{
#ident::#var_ident => {#implementation}
}
}
Fields::Named(fields) => {
let implementation = serialize_named(&fields, name, &tag_type)?;
let field_ident = fields.named.iter().map(|field| &field.ident).collect::<Vec<_>>();
quote!{
#ident::#var_ident{#(#field_ident),*} => {
#implementation
}
}
},
Fields::Unnamed(fields) => quote!{_ => unimplemented!(),},
})
})
.collect::<Result<Vec<_>>>()?;
Ok(quote!{
const _: () = {
impl miniserde::Serialize for #ident {
fn begin(&self) -> miniserde::ser::Fragment {
match self {
#(#begin)*
}
}
}
};
})
}
fn serialize_unit(variant_name: &str, tag_type: &TagType) -> Result<TokenStream> {
Ok(if let TagType::Internal(tag) = &tag_type {
quote!{
struct __Map {
state: miniserde::export::usize,
}
impl miniserde::ser::Map for __Map {
fn next(&mut self) -> miniserde::export::Option<(miniserde::export::Cow<miniserde::export::str>, &dyn miniserde::Serialize)> {
let __state = self.state;
self.state = __state + 1;
match __state {
0 => miniserde::export::Some((
miniserde::export::Cow::Borrowed(#tag),
&#variant_name,
)),
_ => miniserde::export::None,
}
}
}
miniserde::ser::Fragment::Map(miniserde::export::Box::new(__Map {state: 0}))
}
} else {
quote!{miniserde::ser::Fragment::Str(miniserde::export::Cow::Borrowed(#variant_name))}
})
}
fn serialize_named(fields: &FieldsNamed, variant_name: &str, tag_type: &TagType) -> Result<TokenStream> {
let field_ident = fields.named.iter().map(|field| &field.ident).collect::<Vec<_>>();
let field_name = fields.named.iter().map(attr::name_of_field).collect::<Result<Vec<_>>>()?;
let field_type = fields.named.iter().map(|field| &field.ty).collect::<Vec<_>>();
Ok(if let TagType::External = tag_type {
quote!{
use miniserde::Serialize;
#[derive(Serialize)]
struct __AsStruct<'__b> {
#(#field_ident: &'__b #field_type),*,
}
struct __SuperMap<'__b> {
data: __AsStruct<'__b>,
state: miniserde::export::usize,
}
impl<'__a> miniserde::ser::Map for __SuperMap<'__a> {
fn next(&mut self) -> miniserde::export::Option<(miniserde::export::Cow<miniserde::export::str>, &dyn miniserde::Serialize)> {
let __state = self.state;
self.state = __state + 1;
match __state {
0 => miniserde::export::Some((
miniserde::export::Cow::Borrowed(#variant_name),
&self.data,
)),
_ => miniserde::export::None,
}
}
}
miniserde::ser::Fragment::Map(miniserde::export::Box::new(__SuperMap {
data: __AsStruct { #(#field_ident),* },
state: 0,
}))
}
} else {
let (start, tag_arm) = if let TagType::Internal(ref tag) = &tag_type {
(0usize, quote!{
0 => miniserde::export::Some((
miniserde::export::Cow::Borrowed(#tag),
&#variant_name,
)),
})
} else {
(1, quote!())
};
let index = 1usize..;
quote!{
struct __Map<'__a> {
#(#field_ident: &'__a #field_type),*,
state: miniserde::export::usize,
}
impl<'__a> miniserde::ser::Map for __Map<'__a> {
fn next(&mut self) -> miniserde::export::Option<(miniserde::export::Cow<miniserde::export::str>, &dyn miniserde::Serialize)> {
let __state = self.state;
self.state = __state + 1;
match __state {
#tag_arm
#(#index => {
miniserde::export::Some((
miniserde::export::Cow::Borrowed(#field_name),
self.#field_ident,
))
})*,
_ => miniserde::export::None,
}
}
}
miniserde::ser::Fragment::Map(miniserde::export::Box::new(__Map {
#(#field_ident),*,
state: #start,
}))
}
})
}

52
tests/serialize.rs Normal file
View File

@@ -0,0 +1,52 @@
use miniserde::{json, Serialize};
use miniserde_enum::Serialize_enum;
#[test]
fn test_internal() {
#[serde(tag = "type")]
#[derive(Serialize_enum)]
enum Internal {
A,
#[serde(rename = "renamedB")]
B,
C{x: i32},
}
use Internal::*;
let example = [A, B, C{x: 2}];
let actual = json::to_string(&example[..]);
let expected = r#"[{"type":"A"},{"type":"renamedB"},{"type":"C","x":2}]"#;
assert_eq!(actual, expected);
}
#[test]
fn test_external() {
#[derive(Serialize_enum)]
enum External {
A,
#[serde(rename = "renamedB")]
B,
C{x: i32},
}
use External::*;
let example = [A, B, C{x: 2}];
let actual = json::to_string(&example[..]);
let expected = r#"["A","renamedB",{"C":{"x":2}}]"#;
assert_eq!(actual, expected);
}
#[test]
fn test_untagged() {
#[serde(untagged)]
#[derive(Serialize_enum)]
enum Untagged {
A,
#[serde(rename = "renamedB")]
B,
C{x: i32},
}
use Untagged::*;
let example = [A, B, C{x: 2}];
let actual = json::to_string(&example[..]);
let expected = r#"["A","renamedB",{"x":2}]"#;
assert_eq!(actual, expected);
}