From f6e22b3e3116c8224c23ca68075e6e76045392a9 Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Wed, 23 Nov 2022 13:08:06 -0800 Subject: [PATCH] Add support for serialize_with attribute --- instant-xml-macros/src/lib.rs | 28 ++++++++++++++++++++++------ instant-xml-macros/src/ser.rs | 29 +++++++++++++++++++++++------ instant-xml/tests/with.rs | 23 +++++++++++++++++++++++ 3 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 instant-xml/tests/with.rs diff --git a/instant-xml-macros/src/lib.rs b/instant-xml-macros/src/lib.rs index 1eeee94..79c60e0 100644 --- a/instant-xml-macros/src/lib.rs +++ b/instant-xml-macros/src/lib.rs @@ -45,12 +45,6 @@ impl<'input> ContainerMeta<'input> { for (item, span) in meta_items(&input.attrs) { match item { - MetaItem::Attribute => { - return Err(syn::Error::new( - span, - "attribute key invalid in container xml attribute", - )) - } MetaItem::Ns(namespace) => ns = namespace, MetaItem::Rename(lit) => rename = Some(lit), MetaItem::RenameAll(lit) => { @@ -63,6 +57,12 @@ impl<'input> ContainerMeta<'input> { None => mode = Some(new), Some(_) => return Err(syn::Error::new(span, "cannot have two enum modes")), }, + _ => { + return Err(syn::Error::new( + span, + "invalid field in container xml attribute", + )) + } } } @@ -111,6 +111,7 @@ struct FieldMeta { attribute: bool, ns: NamespaceMeta, tag: TokenStream, + serialize_with: Option, } impl FieldMeta { @@ -129,6 +130,7 @@ impl FieldMeta { MetaItem::Attribute => meta.attribute = true, MetaItem::Ns(ns) => meta.ns = ns, MetaItem::Rename(lit) => meta.tag = quote!(#lit), + MetaItem::SerializeWith(lit) => meta.serialize_with = Some(lit), MetaItem::RenameAll(_) => { return Err(syn::Error::new( span, @@ -514,6 +516,8 @@ fn meta_items(attrs: &[syn::Attribute]) -> Vec<(MetaItem, Span)> { } else if id == "wrapped" { items.push((MetaItem::Mode(Mode::Wrapped), span)); MetaState::Comma + } else if id == "serialize_with" { + MetaState::SerializeWith } else { panic!("unexpected key in xml attribute"); } @@ -541,6 +545,13 @@ fn meta_items(attrs: &[syn::Attribute]) -> Vec<(MetaItem, Span)> { items.push((MetaItem::RenameAll(lit), span)); MetaState::Comma } + (MetaState::SerializeWith, TokenTree::Punct(punct)) if punct.as_char() == '=' => { + MetaState::SerializeWithValue + } + (MetaState::SerializeWithValue, TokenTree::Literal(lit)) => { + items.push((MetaItem::SerializeWith(lit), span)); + MetaState::Comma + } (state, tree) => { panic!( "invalid state transition while parsing xml attribute ({}, {tree})", @@ -562,6 +573,8 @@ enum MetaState { RenameValue, RenameAll, RenameAllValue, + SerializeWith, + SerializeWithValue, } impl MetaState { @@ -574,6 +587,8 @@ impl MetaState { MetaState::RenameValue => "RenameValue", MetaState::RenameAll => "RenameAll", MetaState::RenameAllValue => "RenameAllValue", + MetaState::SerializeWith => "SerializeWith", + MetaState::SerializeWithValue => "SerializeWithValue", } } } @@ -635,6 +650,7 @@ enum MetaItem { Rename(Literal), Mode(Mode), RenameAll(Literal), + SerializeWith(Literal), } enum Namespace { diff --git a/instant-xml-macros/src/ser.rs b/instant-xml-macros/src/ser.rs index f5825cf..7f9567d 100644 --- a/instant-xml-macros/src/ser.rs +++ b/instant-xml-macros/src/ser.rs @@ -166,9 +166,11 @@ fn serialize_struct( match data.fields { syn::Fields::Named(ref fields) => { - fields.named.iter().for_each(|field| { - process_named_field(field, &mut body, &mut attributes, &meta); - }); + for field in &fields.named { + if let Err(err) = process_named_field(field, &mut body, &mut attributes, &meta) { + return err.to_compile_error(); + } + } } syn::Fields::Unnamed(_) => todo!(), syn::Fields::Unit => {} @@ -238,16 +240,29 @@ fn process_named_field( body: &mut TokenStream, attributes: &mut TokenStream, meta: &ContainerMeta, -) { +) -> Result<(), syn::Error> { let field_name = field.ident.as_ref().unwrap(); let field_meta = match FieldMeta::from_field(field, meta) { Ok(meta) => meta, Err(err) => { body.extend(err.into_compile_error()); - return; + return Ok(()); } }; + if let Some(with) = field_meta.serialize_with { + let path = with.to_string(); + let path = syn::parse_str::(path.trim_matches('"')).map_err(|err| { + syn::Error::new( + with.span(), + format!("failed to parse serialize_with as path: {err}"), + ) + })?; + + body.extend(quote!(#path(&self.#field_name, serializer)?;)); + return Ok(()); + } + let tag = field_meta.tag; let default_ns = match &meta.ns.uri { Some(ns) => quote!(#ns), @@ -292,7 +307,7 @@ fn process_named_field( #error serializer.write_attr(#tag, #ns, &self.#field_name)?; )); - return; + return Ok(()); } let ns = match field_meta.ns.uri { @@ -315,4 +330,6 @@ fn process_named_field( } } )); + + Ok(()) } diff --git a/instant-xml/tests/with.rs b/instant-xml/tests/with.rs new file mode 100644 index 0000000..6b3813b --- /dev/null +++ b/instant-xml/tests/with.rs @@ -0,0 +1,23 @@ +use std::fmt; + +use instant_xml::{to_string, Error, Serializer, ToXml}; + +#[derive(ToXml)] +struct Foo { + #[xml(serialize_with = "serialize_foo")] + foo: u8, +} + +fn serialize_foo( + value: &u8, + serializer: &mut Serializer<'_, W>, +) -> Result<(), Error> { + serializer.write_str(&format_args!("foo: {value}")) +} + +#[test] +fn serialize_with() { + let v = Foo { foo: 42 }; + let xml = r#"foo: 42"#; + assert_eq!(xml, to_string(&v).unwrap()); +}