xso-proc: Add the default flag to the element meta

Emmanuel Gil Peyrot created

This allows the payload to be absent, and requires the field type to be
Option<minidom::Element>.

Change summary

parsers/src/util/macro_tests.rs | 64 +++++++++++++++++++++++++++++++++++
xso-proc/src/field/element.rs   | 15 ++++++-
xso-proc/src/field/mod.rs       |  7 +++
xso-proc/src/meta.rs            | 13 ++++++
xso/ChangeLog                   |  3 +
xso/src/from_xml_doc.md         | 11 ++++++
6 files changed, 107 insertions(+), 6 deletions(-)

Detailed changes

parsers/src/util/macro_tests.rs 🔗

@@ -1667,6 +1667,48 @@ fn element_catch_one_negative_more_than_one_child() {
     }
 }
 
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct ElementCatchMaybeOne {
+    #[xml(element(default))]
+    maybe_child: core::option::Option<::minidom::Element>,
+}
+
+#[test]
+fn element_catch_maybe_one_roundtrip_none() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<ElementCatchMaybeOne>("<parent xmlns='urn:example:ns1'/>")
+}
+
+#[test]
+fn element_catch_maybe_one_roundtrip_some() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<ElementCatchMaybeOne>(
+        "<parent xmlns='urn:example:ns1'><child><deeper/></child></parent>",
+    )
+}
+
+#[test]
+fn element_catch_maybe_one_negative_more_than_one_child() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<ElementCatchMaybeOne>("<parent xmlns='urn:example:ns1'><child><deeper/></child><child xmlns='urn:example:ns2'/></parent>") {
+        Err(::xso::error::FromElementError::Invalid(::xso::error::Error::Other(e))) if e == "Unknown child in ElementCatchMaybeOne element." => (),
+        other => panic!("unexpected result: {:?}", other),
+    }
+}
+
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
 #[xml(namespace = NS1, name = "parent")]
 struct ElementCatchChildAndOne {
@@ -1689,6 +1731,28 @@ fn element_catch_child_and_one_roundtrip() {
     )
 }
 
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct ElementCatchChildAndMaybeOne {
+    #[xml(child)]
+    child: Empty,
+
+    #[xml(element(default))]
+    element: ::core::option::Option<::minidom::Element>,
+}
+
+#[test]
+fn element_catch_child_and_maybe_one_roundtrip() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<ElementCatchChildAndMaybeOne>(
+        "<parent xmlns='urn:example:ns1'><foo/></parent>",
+    )
+}
+
 #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
 #[xml(namespace = NS1, name = "parent")]
 struct ElementCatchOneAndMany {

xso-proc/src/field/element.rs 🔗

@@ -14,7 +14,7 @@ use quote::quote;
 use syn::*;
 
 use crate::error_message::{self, ParentRef};
-use crate::meta::AmountConstraint;
+use crate::meta::{AmountConstraint, Flag};
 use crate::scope::{AsItemsScope, FromEventsScope};
 use crate::types::{
     as_xml_iter_fn, default_fn, element_ty, from_events_fn, from_xml_builder_ty,
@@ -25,6 +25,10 @@ use crate::types::{
 use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit, NestedMatcher};
 
 pub(super) struct ElementField {
+    /// Flag indicating whether the value should be defaulted if the
+    /// child is absent.
+    pub(super) default_: Flag,
+
     /// Number of child elements allowed.
     pub(super) amount: AmountConstraint,
 }
@@ -58,8 +62,13 @@ impl Field for ElementField {
         match self.amount {
             AmountConstraint::FixedSingle(_) => {
                 let missing_msg = error_message::on_missing_child(container_name, member);
-                let on_absent = quote! {
-                    return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into())
+                let on_absent = match self.default_ {
+                    Flag::Absent => quote! {
+                        return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into())
+                    },
+                    Flag::Present(_) => {
+                        quote! { #default_fn() }
+                    }
                 };
                 Ok(FieldBuilderPart::Nested {
                     extra_defs,

xso-proc/src/field/mod.rs 🔗

@@ -406,7 +406,12 @@ fn new_field(
         }
 
         #[cfg(feature = "minidom")]
-        XmlFieldMeta::Element { span, amount } => Ok(Box::new(ElementField {
+        XmlFieldMeta::Element {
+            span,
+            default_,
+            amount,
+        } => Ok(Box::new(ElementField {
+            default_,
             amount: amount.unwrap_or(AmountConstraint::FixedSingle(span)),
         })),
 

xso-proc/src/meta.rs 🔗

@@ -755,6 +755,9 @@ pub(crate) enum XmlFieldMeta {
         /// This is useful for error messages.
         span: Span,
 
+        /// The `default` flag.
+        default_: Flag,
+
         /// The `n` flag.
         amount: Option<AmountConstraint>,
     },
@@ -1035,9 +1038,16 @@ impl XmlFieldMeta {
     /// Parse a `#[xml(element)]` meta.
     fn element_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
         let mut amount = None;
+        let mut default_ = Flag::Absent;
         if meta.input.peek(syn::token::Paren) {
             meta.parse_nested_meta(|meta| {
-                if meta.path.is_ident("n") {
+                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 if meta.path.is_ident("n") {
                     if amount.is_some() {
                         return Err(Error::new_spanned(meta.path, "duplicate `n` key"));
                     }
@@ -1050,6 +1060,7 @@ impl XmlFieldMeta {
         }
         Ok(Self::Element {
             span: meta.path.span(),
+            default_,
             amount,
         })
     }

xso/ChangeLog 🔗

@@ -22,7 +22,8 @@ Version NEXT:
         structs.
       - Support for collecting all unknown children in a single field as
         collection of `minidom::Element`, or one unknown child as a
-        `minidom::Element`.
+        `minidom::Element`, or zero or one unknown children as an
+        `Option<minidom::Element>`.
       - Support for "transparent" structs (newtype-like patterns for XSO).
       - FromXmlText and AsXmlText are now implemented for jid::NodePart,
         jid::DomainPart, and jid::ResourcePart (!485)

xso/src/from_xml_doc.md 🔗

@@ -433,6 +433,7 @@ The following keys can be used inside the `#[xml(extract(..))]` meta:
 
 | Key | Value type | Description |
 | --- | --- | --- |
+| `default` | flag | If present, an absent child will substitute the default value instead of raising an error. |
 | `n` | `1` or `..` | If `1`, a single element is parsed. If `..`, a collection is parsed. Defaults to `1`. |
 
 When parsing a single child element (i.e. `n = 1` or no `n` value set at all),
@@ -446,6 +447,16 @@ addition, the field's type must implement
 field's reference type must implement
 `IntoIterator<Item = &'_ minidom::Element>` to derive `AsXml`.
 
+If `default` is specified and the child is absent in the source, the value
+is generated using [`core::default::Default`]. `default` has no influence on
+`AsXml`. Combining `default` and `n` where `n` is not set to `1` is not
+supported and will cause a compile-time error.
+
+Using `default` with a type other than `Option<T>` will cause the
+serialisation to mismatch the deserialisation (i.e. the struct is then not
+roundtrip-safe), because the deserialisation does not compare the value
+against `default` (but has special provisions to work with `Option<T>`).
+
 Fields with the `element` meta are deserialised with the lowest priority.
 While other fields are processed in the order they are declared, `element`
 fields may capture arbitrary child elements, so they are considered as the