diff --git a/instant-xml-macros/src/de.rs b/instant-xml-macros/src/de.rs index a97cc73..4ba665e 100644 --- a/instant-xml-macros/src/de.rs +++ b/instant-xml-macros/src/de.rs @@ -2,7 +2,9 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::spanned::Spanned; -use super::{discard_lifetimes, ContainerMeta, FieldMeta, Namespace, VariantMeta}; +use super::{ + discard_lifetimes, meta_items, ContainerMeta, FieldMeta, Mode, Namespace, VariantMeta, +}; pub(crate) fn from_xml(input: &syn::DeriveInput) -> TokenStream { let meta = match ContainerMeta::from_derive(input) { @@ -10,21 +12,21 @@ pub(crate) fn from_xml(input: &syn::DeriveInput) -> TokenStream { Err(e) => return e.to_compile_error(), }; - match &input.data { - syn::Data::Struct(_) if meta.mode.is_some() => { + match (&input.data, meta.mode) { + (syn::Data::Struct(data), None) => deserialize_struct(input, data, meta), + (syn::Data::Enum(data), Some(Mode::Scalar)) => deserialize_scalar_enum(input, data, meta), + (syn::Data::Enum(data), Some(Mode::Wrapped)) => deserialize_wrapped_enum(input, data, meta), + (syn::Data::Struct(_), _) => { syn::Error::new(input.span(), "no enum mode allowed on struct type").to_compile_error() } - syn::Data::Struct(ref data) => deserialize_struct(input, data, meta), - syn::Data::Enum(_) if meta.mode.is_none() => { + (syn::Data::Enum(_), None) => { syn::Error::new(input.span(), "missing enum mode").to_compile_error() } - syn::Data::Enum(ref data) => deserialize_enum(input, data, meta), _ => todo!(), } } -#[rustfmt::skip] -fn deserialize_enum( +fn deserialize_scalar_enum( input: &syn::DeriveInput, data: &syn::DataEnum, meta: ContainerMeta, @@ -36,7 +38,7 @@ fn deserialize_enum( let v_ident = &variant.ident; let meta = match VariantMeta::from_variant(variant, &meta) { Ok(meta) => meta, - Err(err) => return err.to_compile_error() + Err(err) => return err.to_compile_error(), }; let serialize_as = meta.serialize_as; @@ -63,6 +65,96 @@ fn deserialize_enum( ) } +fn deserialize_wrapped_enum( + input: &syn::DeriveInput, + data: &syn::DataEnum, + meta: ContainerMeta, +) -> TokenStream { + if data.variants.is_empty() { + return syn::Error::new(input.span(), "empty enum is not supported").to_compile_error(); + } + + let ident = &input.ident; + let mut variants = TokenStream::new(); + for variant in data.variants.iter() { + let field = match &variant.fields { + syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + fields.unnamed.first().unwrap() + } + _ => { + return syn::Error::new( + input.span(), + "wrapped enum variants must have 1 unnamed field", + ) + .to_compile_error() + } + }; + + if !meta_items(&variant.attrs).is_empty() { + return syn::Error::new( + input.span(), + "attributes not allowed on wrapped enum variants", + ) + .to_compile_error(); + } + + let mut no_lifetime_type = field.ty.clone(); + discard_lifetimes(&mut no_lifetime_type); + + if !variants.is_empty() { + variants.extend(quote!(else)); + } + + let v_ident = &variant.ident; + variants.extend( + quote!(if ::instant_xml::Kind::Element(id) == <#no_lifetime_type as FromXml>::KIND { + let mut nested = deserializer.nested(data); + #ident::#v_ident(#no_lifetime_type::deserialize(&mut nested)?) + }), + ); + } + + let name = meta.tag(); + let default_namespace = meta.default_namespace(); + let generics = meta.xml_generics(); + let (xml_impl_generics, _, _) = generics.split_for_impl(); + let (_, ty_generics, where_clause) = input.generics.split_for_impl(); + quote!( + impl #xml_impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { + fn deserialize<'cx>(deserializer: &'cx mut ::instant_xml::Deserializer<'cx, 'xml>) -> Result { + use ::instant_xml::de::Node; + use ::instant_xml::Error; + + let node = match deserializer.next() { + Some(result) => result?, + None => return Err(Error::MissingValue), + }; + + let data = match node { + Node::Open(data) => data, + _ => return Err(Error::UnexpectedState), + }; + + let id = deserializer.element_id(&data)?; + let value = #variants else { + return Err(Error::UnexpectedTag); + }; + + if let Some(_) = deserializer.next() { + return Err(Error::UnexpectedState); + } + + Ok(value) + } + + const KIND: ::instant_xml::Kind<'static> = ::instant_xml::Kind::Element(::instant_xml::Id { + ns: #default_namespace, + name: #name, + }); + } + ) +} + fn deserialize_struct( input: &syn::DeriveInput, data: &syn::DataStruct, @@ -143,7 +235,7 @@ fn deserialize_struct( quote!( impl #xml_impl_generics FromXml<'xml> for #ident #ty_generics #where_clause { fn deserialize<'cx>(deserializer: &'cx mut ::instant_xml::Deserializer<'cx, 'xml>) -> Result { - use ::instant_xml::de::{Deserializer, Node}; + use ::instant_xml::de::Node; use ::instant_xml::{Error, Id}; use ::core::marker::PhantomData; diff --git a/instant-xml-macros/src/lib.rs b/instant-xml-macros/src/lib.rs index e21363d..527a256 100644 --- a/instant-xml-macros/src/lib.rs +++ b/instant-xml-macros/src/lib.rs @@ -59,8 +59,8 @@ impl<'input> ContainerMeta<'input> { Err(err) => return Err(syn::Error::new(span, err)), }; } - MetaItem::Scalar => match mode { - None => mode = Some(Mode::Scalar), + MetaItem::Mode(new) => match mode { + None => mode = Some(new), Some(_) => return Err(syn::Error::new(span, "cannot have two enum modes")), }, } @@ -135,7 +135,7 @@ impl FieldMeta { "attribute 'rename_all' invalid in field xml attribute", )) } - MetaItem::Scalar => { + MetaItem::Mode(_) => { return Err(syn::Error::new(span, "invalid attribute for struct field")); } } @@ -509,7 +509,10 @@ fn meta_items(attrs: &[syn::Attribute]) -> Vec<(MetaItem, Span)> { } else if id == "rename_all" { MetaState::RenameAll } else if id == "scalar" { - items.push((MetaItem::Scalar, span)); + items.push((MetaItem::Mode(Mode::Scalar), span)); + MetaState::Comma + } else if id == "wrapped" { + items.push((MetaItem::Mode(Mode::Wrapped), span)); MetaState::Comma } else { panic!("unexpected key in xml attribute"); @@ -630,7 +633,7 @@ enum MetaItem { Attribute, Ns(NamespaceMeta), Rename(Literal), - Scalar, + Mode(Mode), RenameAll(Literal), } @@ -697,8 +700,10 @@ fn discard_path_lifetimes(path: &mut syn::TypePath) { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Mode { Scalar, + Wrapped, } #[cfg(test)] diff --git a/instant-xml-macros/src/ser.rs b/instant-xml-macros/src/ser.rs index 3bbc88f..f5825cf 100644 --- a/instant-xml-macros/src/ser.rs +++ b/instant-xml-macros/src/ser.rs @@ -2,9 +2,8 @@ use proc_macro2::TokenStream; use quote::quote; use syn::spanned::Spanned; -use crate::Namespace; - -use super::{discard_lifetimes, ContainerMeta, FieldMeta, VariantMeta}; +use super::{discard_lifetimes, meta_items, ContainerMeta, FieldMeta, Mode, VariantMeta}; +use crate::{case::RenameRule, Namespace}; pub fn to_xml(input: &syn::DeriveInput) -> proc_macro2::TokenStream { let meta = match ContainerMeta::from_derive(input) { @@ -12,21 +11,21 @@ pub fn to_xml(input: &syn::DeriveInput) -> proc_macro2::TokenStream { Err(e) => return e.to_compile_error(), }; - match &input.data { - syn::Data::Struct(_) if meta.mode.is_some() => { + match (&input.data, meta.mode) { + (syn::Data::Struct(data), None) => serialize_struct(input, data, meta), + (syn::Data::Enum(data), Some(Mode::Scalar)) => serialize_scalar_enum(input, data, meta), + (syn::Data::Enum(data), Some(Mode::Wrapped)) => serialize_wrapped_enum(input, data, meta), + (syn::Data::Struct(_), _) => { syn::Error::new(input.span(), "enum mode not allowed on struct type").to_compile_error() } - syn::Data::Struct(ref data) => serialize_struct(input, data, meta), - syn::Data::Enum(_) if meta.mode.is_none() => { - syn::Error::new(input.span(), "missing enum mode") - .to_compile_error() + (syn::Data::Enum(_), _) => { + syn::Error::new(input.span(), "missing enum mode").to_compile_error() } - syn::Data::Enum(ref data) => serialize_enum(input, data, meta), _ => todo!(), } } -fn serialize_enum( +fn serialize_scalar_enum( input: &syn::DeriveInput, data: &syn::DataEnum, meta: ContainerMeta, @@ -35,12 +34,12 @@ fn serialize_enum( let mut variants = TokenStream::new(); for variant in data.variants.iter() { - let v_ident = &variant.ident; let meta = match VariantMeta::from_variant(variant, &meta) { Ok(meta) => meta, Err(err) => return err.to_compile_error(), }; + let v_ident = &variant.ident; let serialize_as = meta.serialize_as; variants.extend(quote!(#ident::#v_ident => #serialize_as,)); } @@ -60,6 +59,103 @@ fn serialize_enum( ) } +fn serialize_wrapped_enum( + input: &syn::DeriveInput, + data: &syn::DataEnum, + meta: ContainerMeta, +) -> TokenStream { + if meta.rename_all != RenameRule::None { + return syn::Error::new( + input.span(), + "rename_all is not allowed on wrapped enum type", + ) + .to_compile_error(); + } + + let ident = &input.ident; + let mut variants = TokenStream::new(); + for variant in data.variants.iter() { + match &variant.fields { + syn::Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {} + _ => { + return syn::Error::new( + input.span(), + "wrapped enum variants must have 1 unnamed field", + ) + .to_compile_error() + } + } + + if !meta_items(&variant.attrs).is_empty() { + return syn::Error::new( + input.span(), + "attributes not allowed on wrapped enum variants", + ) + .to_compile_error(); + } + + let v_ident = &variant.ident; + variants.extend(quote!(#ident::#v_ident(inner) => inner.serialize(serializer)?,)); + } + + let default_namespace = meta.default_namespace(); + let cx_len = meta.ns.prefixes.len(); + let mut context = quote!( + let mut new = ::instant_xml::ser::Context::<#cx_len>::default(); + new.default_ns = #default_namespace; + ); + + for (i, (prefix, ns)) in meta.ns.prefixes.iter().enumerate() { + context.extend(quote!( + new.prefixes[#i] = ::instant_xml::ser::Prefix { ns: #ns, prefix: #prefix }; + )); + } + + let mut generics = input.generics.clone(); + for param in generics.type_params_mut() { + param + .bounds + .push(syn::parse_str("::instant_xml::ToXml").unwrap()); + } + + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let tag = meta.tag(); + quote!( + impl #impl_generics ToXml for #ident #ty_generics #where_clause { + fn serialize( + &self, + serializer: &mut instant_xml::Serializer, + ) -> Result<(), instant_xml::Error> { + // Start tag + let prefix = serializer.write_start(#tag, #default_namespace, false)?; + debug_assert_eq!(prefix, None); + + // Set up element context, this will also emit namespace declarations + #context + let old = serializer.push(new)?; + + // Finalize start element + serializer.end_start()?; + + match self { + #variants + } + + // Close tag + serializer.write_close(prefix, #tag)?; + serializer.pop(old); + + Ok(()) + } + + const KIND: ::instant_xml::Kind<'static> = ::instant_xml::Kind::Element(::instant_xml::Id { + ns: #default_namespace, + name: #tag, + }); + }; + ) +} + fn serialize_struct( input: &syn::DeriveInput, data: &syn::DataStruct, diff --git a/instant-xml/src/lib.rs b/instant-xml/src/lib.rs index 8ef2c4f..cf4281e 100644 --- a/instant-xml/src/lib.rs +++ b/instant-xml/src/lib.rs @@ -110,6 +110,7 @@ pub enum Error { DuplicateValue, } +#[derive(Eq, PartialEq)] pub enum Kind<'a> { Scalar, Element(Id<'a>), diff --git a/instant-xml/tests/wrapped-enum.rs b/instant-xml/tests/wrapped-enum.rs new file mode 100644 index 0000000..a8d4412 --- /dev/null +++ b/instant-xml/tests/wrapped-enum.rs @@ -0,0 +1,26 @@ +use instant_xml::{from_str, to_string, FromXml, ToXml}; + +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] +#[xml(wrapped)] +enum Foo { + Bar(Bar), + Baz(Baz), +} + +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] +struct Bar { + bar: u8, +} + +#[derive(Debug, Eq, FromXml, PartialEq, ToXml)] +struct Baz { + baz: String, +} + +#[test] +fn wrapped_enum() { + let v = Foo::Bar(Bar { bar: 42 }); + let xml = r#"42"#; + assert_eq!(xml, to_string(&v).unwrap()); + assert_eq!(v, from_str(xml).unwrap()); +}