Implement serialization for unit variants and named variants
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
18
Cargo.toml
Normal file
18
Cargo.toml
Normal 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
97
src/attr.rs
Normal 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
51
src/lib.rs
Normal 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
168
src/ser.rs
Normal 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
52
tests/serialize.rs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user