xso-proc: add support for non-String typed attributes

Jonas SchΓ€fer created

Change summary

parsers/src/util/macro_tests.rs | 17 +++++++
xso-proc/src/field.rs           | 14 ++++-
xso-proc/src/types.rs           | 76 ++++++++++++++++++++++++++++++++++
xso/src/from_xml_doc.md         |  3 
4 files changed, 104 insertions(+), 6 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs πŸ”—

@@ -313,3 +313,20 @@ fn prefixed_attribute_roundtrip() {
     };
     roundtrip_full::<PrefixedAttribute>("<attr xmlns='urn:example:ns1' xml:lang='foo'/>");
 }
+
+#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "attr")]
+struct RequiredNonStringAttribute {
+    #[xml(attribute)]
+    foo: i32,
+}
+
+#[test]
+fn required_non_string_attribute_roundtrip() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<RequiredNonStringAttribute>("<attr xmlns='urn:example:ns1' foo='-16'/>");
+}

xso-proc/src/field.rs πŸ”—

@@ -15,6 +15,7 @@ use rxml_validation::NcName;
 use crate::error_message::{self, ParentRef};
 use crate::meta::{NameRef, NamespaceRef, XmlFieldMeta};
 use crate::scope::{FromEventsScope, IntoEventsScope};
+use crate::types::{from_xml_text_fn, into_optional_xml_text_fn};
 
 /// Code slices necessary for declaring and initializing a temporary variable
 /// for parsing purposes.
@@ -182,6 +183,7 @@ impl FieldDef {
                 ref xml_namespace,
             } => {
                 let FromEventsScope { ref attrs, .. } = scope;
+                let ty = self.ty.clone();
 
                 let missing_msg = error_message::on_missing_attribute(container_name, &self.member);
 
@@ -192,15 +194,17 @@ impl FieldDef {
                     },
                 };
 
+                let from_xml_text = from_xml_text_fn(ty.clone());
+
                 return Ok(FieldBuilderPart::Init {
                     value: FieldTempInit {
-                        ty: self.ty.clone(),
                         init: quote! {
-                            match #attrs.remove(#xml_namespace, #xml_name) {
+                            match #attrs.remove(#xml_namespace, #xml_name).map(#from_xml_text).transpose()? {
                                 ::core::option::Option::Some(v) => v,
                                 ::core::option::Option::None => return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()),
                             }
                         },
+                        ty: self.ty.clone(),
                     },
                 });
             }
@@ -230,13 +234,15 @@ impl FieldDef {
                     },
                 };
 
+                let into_optional_xml_text = into_optional_xml_text_fn(self.ty.clone());
+
                 return Ok(FieldIteratorPart::Header {
                     setter: quote! {
-                        #attrs.insert(
+                        #into_optional_xml_text(#bound_name)?.and_then(|#bound_name| #attrs.insert(
                             #xml_namespace,
                             #xml_name.to_owned(),
                             #bound_name,
-                        );
+                        ));
                     },
                 });
             }

xso-proc/src/types.rs πŸ”—

@@ -7,7 +7,7 @@
 //! Module with specific [`syn::Type`] constructors.
 
 use proc_macro2::Span;
-use syn::*;
+use syn::{spanned::Spanned, *};
 
 /// Construct a [`syn::Type`] referring to `::xso::exports::rxml::QName`.
 pub(crate) fn qname_ty(span: Span) -> Type {
@@ -40,3 +40,77 @@ pub(crate) fn qname_ty(span: Span) -> Type {
         },
     })
 }
+
+/// Construct a [`syn::Expr`] referring to
+/// `<#ty as ::xso::FromXmlText>::from_xml_text`.
+pub(crate) fn from_xml_text_fn(ty: Type) -> Expr {
+    let span = ty.span();
+    Expr::Path(ExprPath {
+        attrs: Vec::new(),
+        qself: Some(QSelf {
+            lt_token: syn::token::Lt { spans: [span] },
+            ty: Box::new(ty),
+            position: 2,
+            as_token: Some(syn::token::As { span }),
+            gt_token: syn::token::Gt { spans: [span] },
+        }),
+        path: Path {
+            leading_colon: Some(syn::token::PathSep {
+                spans: [span, span],
+            }),
+            segments: [
+                PathSegment {
+                    ident: Ident::new("xso", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("FromXmlText", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("from_xml_text", span),
+                    arguments: PathArguments::None,
+                },
+            ]
+            .into_iter()
+            .collect(),
+        },
+    })
+}
+
+/// Construct a [`syn::Expr`] referring to
+/// `<#ty as ::xso::IntoOptionalXmlText>::into_optional_xml_text`.
+pub(crate) fn into_optional_xml_text_fn(ty: Type) -> Expr {
+    let span = ty.span();
+    Expr::Path(ExprPath {
+        attrs: Vec::new(),
+        qself: Some(QSelf {
+            lt_token: syn::token::Lt { spans: [span] },
+            ty: Box::new(ty),
+            position: 2,
+            as_token: Some(syn::token::As { span }),
+            gt_token: syn::token::Gt { spans: [span] },
+        }),
+        path: Path {
+            leading_colon: Some(syn::token::PathSep {
+                spans: [span, span],
+            }),
+            segments: [
+                PathSegment {
+                    ident: Ident::new("xso", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("IntoOptionalXmlText", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("into_optional_xml_text", span),
+                    arguments: PathArguments::None,
+                },
+            ]
+            .into_iter()
+            .collect(),
+        },
+    })
+}

xso/src/from_xml_doc.md πŸ”—

@@ -63,7 +63,8 @@ The following mapping types are defined:
 #### `attribute` meta
 
 The `attribute` meta causes the field to be mapped to an XML attribute of the
-same name. The field must be of type [`String`].
+same name. For `FromXml`, the field's type must implement [`FromXmlText`] and
+for `IntoXml`, the field's type must implement [`IntoOptionalXmlText`].
 
 The following keys can be used inside the `#[xml(attribute(..))]` meta: