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

This allows the payload to be absent, and requires the field type to be
Option<minidom::Element>.
This commit is contained in:
Emmanuel Gil Peyrot 2024-12-20 18:41:39 +01:00 • committed by Jonas Schäfer
commit 1823afbc71
6 changed files with 107 additions and 6 deletions

View file

@ -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)] #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
#[xml(namespace = NS1, name = "parent")] #[xml(namespace = NS1, name = "parent")]
struct ElementCatchChildAndOne { 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)] #[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
#[xml(namespace = NS1, name = "parent")] #[xml(namespace = NS1, name = "parent")]
struct ElementCatchOneAndMany { struct ElementCatchOneAndMany {

View file

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

View file

@ -406,7 +406,12 @@ fn new_field(
} }
#[cfg(feature = "minidom")] #[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)), amount: amount.unwrap_or(AmountConstraint::FixedSingle(span)),
})), })),

View file

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

View file

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

View file

@ -433,6 +433,7 @@ The following keys can be used inside the `#[xml(extract(..))]` meta:
| Key | Value type | Description | | 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`. | | `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), 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 field's reference type must implement
`IntoIterator<Item = &'_ minidom::Element>` to derive `AsXml`. `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. Fields with the `element` meta are deserialised with the lowest priority.
While other fields are processed in the order they are declared, `element` While other fields are processed in the order they are declared, `element`
fields may capture arbitrary child elements, so they are considered as the fields may capture arbitrary child elements, so they are considered as the