diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 5f5c667f7f567eaad41a63407ce524cd08d48e07..65114e0b771a8ca226ff82cc1f615c79509d3913 100644 --- a/parsers/src/util/macro_tests.rs +++ b/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::("") +} + +#[test] +fn element_catch_maybe_one_roundtrip_some() { + #[allow(unused_imports)] + use core::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::( + "", + ) +} + +#[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::("") { + 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::( + "", + ) +} + #[derive(FromXml, AsXml, PartialEq, Debug, Clone)] #[xml(namespace = NS1, name = "parent")] struct ElementCatchOneAndMany { diff --git a/xso-proc/src/field/element.rs b/xso-proc/src/field/element.rs index 2df6f4070f7cd7bbaa73f294c15b9294de2b1adf..16f482224809bbeee23f73e7c462939236bd325d 100644 --- a/xso-proc/src/field/element.rs +++ b/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, diff --git a/xso-proc/src/field/mod.rs b/xso-proc/src/field/mod.rs index 1ef52b8e165a4639cf39b7b0399e5153254e997a..fda9438ab05200e6ed2ff6f46e9e087e7c00f875 100644 --- a/xso-proc/src/field/mod.rs +++ b/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)), })), diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index 751940b515c04d86398a7605c44728525ee3a0b1..68068ec8d3db37423cb792cbeebc7f31468eda67 100644 --- a/xso-proc/src/meta.rs +++ b/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, }, @@ -1035,9 +1038,16 @@ impl XmlFieldMeta { /// Parse a `#[xml(element)]` meta. fn element_from_meta(meta: ParseNestedMeta<'_>) -> Result { 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, }) } diff --git a/xso/ChangeLog b/xso/ChangeLog index 376af4f0d8f7f7be1769772fdba6d551935a45d3..44c0d278b650335dc4c530fa941c5c33f06f8c03 100644 --- a/xso/ChangeLog +++ b/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`. - Support for "transparent" structs (newtype-like patterns for XSO). - FromXmlText and AsXmlText are now implemented for jid::NodePart, jid::DomainPart, and jid::ResourcePart (!485) diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 720f99fb800e2bbc0cd33c8541b20d4626263e4b..a495f825a750a15688e0d41327e95abfef12eee6 100644 --- a/xso/src/from_xml_doc.md +++ b/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` 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` 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`). + 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