xso-proc: add support for defaulting in attribute parsing

Jonas SchΓ€fer created

Change summary

parsers/src/util/macro_tests.rs | 50 +++++++++++++++++++++++++++++++++++
xso-proc/src/field.rs           | 29 ++++++++++++++++++--
xso-proc/src/meta.rs            | 46 ++++++++++++++++++++++++++++++++
xso-proc/src/types.rs           | 41 ++++++++++++++++++++++++++++
xso/src/from_xml_doc.md         | 15 ++++++++++
5 files changed, 178 insertions(+), 3 deletions(-)

Detailed changes

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

@@ -330,3 +330,53 @@ fn required_non_string_attribute_roundtrip() {
     };
     roundtrip_full::<RequiredNonStringAttribute>("<attr xmlns='urn:example:ns1' foo='-16'/>");
 }
+
+#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "attr")]
+struct DefaultAttribute {
+    #[xml(attribute(default))]
+    foo: std::option::Option<String>,
+
+    #[xml(attribute(default))]
+    bar: std::option::Option<u16>,
+}
+
+#[test]
+fn default_attribute_roundtrip_aa() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1'/>");
+}
+
+#[test]
+fn default_attribute_roundtrip_pa() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1' foo='xyz'/>");
+}
+
+#[test]
+fn default_attribute_roundtrip_ap() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1' bar='16'/>");
+}
+
+#[test]
+fn default_attribute_roundtrip_pp() {
+    #[allow(unused_imports)]
+    use std::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1' foo='xyz' bar='16'/>");
+}

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

@@ -13,9 +13,9 @@ use syn::{spanned::Spanned, *};
 use rxml_validation::NcName;
 
 use crate::error_message::{self, ParentRef};
-use crate::meta::{NameRef, NamespaceRef, XmlFieldMeta};
+use crate::meta::{Flag, NameRef, NamespaceRef, XmlFieldMeta};
 use crate::scope::{FromEventsScope, IntoEventsScope};
-use crate::types::{from_xml_text_fn, into_optional_xml_text_fn};
+use crate::types::{default_fn, from_xml_text_fn, into_optional_xml_text_fn};
 
 /// Code slices necessary for declaring and initializing a temporary variable
 /// for parsing purposes.
@@ -67,6 +67,10 @@ enum FieldKind {
 
         /// The XML name of the attribute.
         xml_name: NameRef,
+
+        // Flag indicating whether the value should be defaulted if the
+        // attribute is absent.
+        default_: Flag,
     },
 }
 
@@ -81,6 +85,7 @@ impl FieldKind {
                 span,
                 namespace,
                 name,
+                default_,
             } => {
                 let xml_name = match name {
                     Some(v) => v,
@@ -107,6 +112,7 @@ impl FieldKind {
                 Ok(Self::Attribute {
                     xml_name,
                     xml_namespace: namespace,
+                    default_,
                 })
             }
         }
@@ -181,6 +187,7 @@ impl FieldDef {
             FieldKind::Attribute {
                 ref xml_name,
                 ref xml_namespace,
+                ref default_,
             } => {
                 let FromEventsScope { ref attrs, .. } = scope;
                 let ty = self.ty.clone();
@@ -196,12 +203,24 @@ impl FieldDef {
 
                 let from_xml_text = from_xml_text_fn(ty.clone());
 
+                let on_absent = match default_ {
+                    Flag::Absent => quote! {
+                        return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into())
+                    },
+                    Flag::Present(_) => {
+                        let default_ = default_fn(ty.clone());
+                        quote! {
+                            #default_()
+                        }
+                    }
+                };
+
                 return Ok(FieldBuilderPart::Init {
                     value: FieldTempInit {
                         init: quote! {
                             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()),
+                                ::core::option::Option::None => #on_absent,
                             }
                         },
                         ty: self.ty.clone(),
@@ -224,6 +243,7 @@ impl FieldDef {
             FieldKind::Attribute {
                 ref xml_name,
                 ref xml_namespace,
+                ..
             } => {
                 let IntoEventsScope { ref attrs, .. } = scope;
 
@@ -237,6 +257,9 @@ impl FieldDef {
                 let into_optional_xml_text = into_optional_xml_text_fn(self.ty.clone());
 
                 return Ok(FieldIteratorPart::Header {
+                    // This is a neat little trick:
+                    // Option::from(x) converts x to an Option<T> *unless* it
+                    // already is an Option<_>.
                     setter: quote! {
                         #into_optional_xml_text(#bound_name)?.and_then(|#bound_name| #attrs.insert(
                             #xml_namespace,

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

@@ -108,6 +108,39 @@ impl quote::ToTokens for NameRef {
     }
 }
 
+/// Represents a boolean flag from a `#[xml(..)]` attribute meta.
+#[derive(Clone, Copy, Debug)]
+pub(crate) enum Flag {
+    /// The flag is not set.
+    Absent,
+
+    /// The flag was set.
+    Present(
+        /// The span of the syntax element which enabled the flag.
+        ///
+        /// This is used to generate useful error messages by pointing at the
+        /// specific place the flag was activated.
+        #[allow(dead_code)]
+        Span,
+    ),
+}
+
+impl Flag {
+    /// Return true if the flag is set, false otherwise.
+    pub(crate) fn is_set(&self) -> bool {
+        match self {
+            Self::Absent => false,
+            Self::Present(_) => true,
+        }
+    }
+}
+
+impl<T: Spanned> From<T> for Flag {
+    fn from(other: T) -> Flag {
+        Flag::Present(other.span())
+    }
+}
+
 /// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
 #[derive(Debug)]
 pub(crate) struct XmlCompoundMeta {
@@ -261,6 +294,9 @@ pub(crate) enum XmlFieldMeta {
 
         /// The XML name supplied.
         name: Option<NameRef>,
+
+        /// The `default` flag.
+        default_: Flag,
     },
 }
 
@@ -279,11 +315,13 @@ impl XmlFieldMeta {
                 span: meta.path.span(),
                 name: Some(name),
                 namespace,
+                default_: Flag::Absent,
             })
         } else if meta.input.peek(syn::token::Paren) {
             // full syntax
             let mut name: Option<NameRef> = None;
             let mut namespace: Option<NamespaceRef> = None;
+            let mut default_ = Flag::Absent;
             meta.parse_nested_meta(|meta| {
                 if meta.path.is_ident("name") {
                     if name.is_some() {
@@ -312,6 +350,12 @@ impl XmlFieldMeta {
                     }
                     namespace = Some(meta.value()?.parse()?);
                     Ok(())
+                } else if meta.path.is_ident("default") {
+                    if default_.is_set() {
+                        return Err(Error::new_spanned(meta.path, "duplicate `default` key"));
+                    }
+                    default_ = (&meta.path).into();
+                    Ok(())
                 } else {
                     Err(Error::new_spanned(meta.path, "unsupported key"))
                 }
@@ -320,6 +364,7 @@ impl XmlFieldMeta {
                 span: meta.path.span(),
                 name,
                 namespace,
+                default_,
             })
         } else {
             // argument-less syntax
@@ -327,6 +372,7 @@ impl XmlFieldMeta {
                 span: meta.path.span(),
                 name: None,
                 namespace: None,
+                default_: Flag::Absent,
             })
         }
     }

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

@@ -114,3 +114,44 @@ pub(crate) fn into_optional_xml_text_fn(ty: Type) -> Expr {
         },
     })
 }
+
+/// Construct a [`syn::Expr`] referring to
+/// `<#of_ty as ::std::default::Default>::default`.
+pub(crate) fn default_fn(of_ty: Type) -> Expr {
+    let span = of_ty.span();
+    Expr::Path(ExprPath {
+        attrs: Vec::new(),
+        qself: Some(QSelf {
+            lt_token: syn::token::Lt { spans: [span] },
+            ty: Box::new(of_ty),
+            position: 3,
+            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("std", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("default", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("Default", span),
+                    arguments: PathArguments::None,
+                },
+                PathSegment {
+                    ident: Ident::new("default", span),
+                    arguments: PathArguments::None,
+                },
+            ]
+            .into_iter()
+            .collect(),
+        },
+    })
+}

xso/src/from_xml_doc.md πŸ”—

@@ -28,6 +28,15 @@ syntax construct *meta*.
 All key-value pairs interpreted by these derive macros must be wrapped in a
 `#[xml( ... )]` *meta*.
 
+The values associated with the keys may be of different types, defined as
+such:
+
+- *path*: A Rust path, like `some_crate::foo::Bar`. Note that `foo` on its own
+  is also a path.
+- *string literal*: A string literal, like `"hello world!"`.
+- flag: Has no value. The key's mere presence has relevance and it must not be
+  followed by a `=` sign.
+
 ### Struct meta
 
 The following keys are defined on structs:
@@ -72,6 +81,7 @@ The following keys can be used inside the `#[xml(attribute(..))]` meta:
 | --- | --- | --- |
 | `namespace` | *string literal* or *path* | The optional namespace of the XML attribute to match. If it is a *path*, it must point at a `&'static str`. Note that attributes, unlike elements, are unnamespaced by default. |
 | `name` | *string literal* or *path* | The name of the XML attribute to match. If it is a *path*, it must point at a `&'static NcNameStr`. |
+| `default` | flag | If present, an absent attribute will substitute the default value instead of raising an error. |
 
 If the `name` key contains a namespace prefix, it must be one of the prefixes
 defined as built-in in the XML specifications. That prefix will then be
@@ -84,6 +94,11 @@ The `attribute` meta also supports a shorthand syntax,
 `name` key (with optional prefix as described above, and unnamespaced
 otherwise).
 
+If `default` is specified and the attribute is absent in the source, the value
+is generated using [`std::default::Default`], requiring the field type to
+implement the `Default` trait for a `FromXml` derivation. `default` has no
+influence on `IntoXml`.
+
 ##### Example
 
 ```rust