commit 353d0c07d4fee96992ecce109259f420f8bb014f Author: Aymeric Beringer Date: Fri Oct 11 16:32:09 2019 +0200 Implement serialization for unit variants and named variants diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..85a4534 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "miniserde-enum" +version = "0.1.0" +authors = ["Aymeric Beringer "] +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 diff --git a/src/attr.rs b/src/attr.rs new file mode 100644 index 0000000..d7ee3a6 --- /dev/null +++ b/src/attr.rs @@ -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 { + 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> { + 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 { + 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 { + let rename = attr_rename(&var.attrs)?; + Ok(rename.unwrap_or_else(|| var.ident.to_string())) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..84f12b0 --- /dev/null +++ b/src/lib.rs @@ -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, +} + +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() +} diff --git a/src/ser.rs b/src/ser.rs new file mode 100644 index 0000000..62a1ad8 --- /dev/null +++ b/src/ser.rs @@ -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 { + 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 = 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::>>()?; + 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::>(); + + quote!{ + #ident::#var_ident{#(#field_ident),*} => { + #implementation + } + } + }, + Fields::Unnamed(fields) => quote!{_ => unimplemented!(),}, + }) + }) + .collect::>>()?; + + 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 { + 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, &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 { + let field_ident = fields.named.iter().map(|field| &field.ident).collect::>(); + let field_name = fields.named.iter().map(attr::name_of_field).collect::>>()?; + let field_type = fields.named.iter().map(|field| &field.ty).collect::>(); + 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, &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, &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, + })) + } + }) +} diff --git a/tests/serialize.rs b/tests/serialize.rs new file mode 100644 index 0000000..27f7fe8 --- /dev/null +++ b/tests/serialize.rs @@ -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); +}