diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 19775103..1bc559ea 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -521,3 +521,32 @@ fn parent_positive() { .unwrap(); assert_eq!(v.child.foo, "hello world!"); } + +#[derive(FromXml, AsXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, name = "parent")] +struct OptionalChild { + #[xml(child(default))] + child: std::option::Option, +} + +#[test] +fn optional_child_roundtrip_present() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::( + "", + ) +} + +#[test] +fn optional_child_roundtrip_absent() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::("") +} diff --git a/xso-proc/src/field.rs b/xso-proc/src/field.rs index 50414cb8..30ed89e2 100644 --- a/xso-proc/src/field.rs +++ b/xso-proc/src/field.rs @@ -152,7 +152,11 @@ enum FieldKind { }, /// The field maps to a child - Child, + Child { + // Flag indicating whether the value should be defaulted if the + // child is absent. + default_: Flag, + }, } impl FieldKind { @@ -199,7 +203,7 @@ impl FieldKind { XmlFieldMeta::Text { codec } => Ok(Self::Text { codec }), - XmlFieldMeta::Child => Ok(Self::Child), + XmlFieldMeta::Child { default_ } => Ok(Self::Child { default_ }), } } } @@ -341,7 +345,7 @@ impl FieldDef { }) } - FieldKind::Child => { + FieldKind::Child { ref default_ } => { let FromEventsScope { ref substate_result, .. @@ -353,6 +357,18 @@ impl FieldDef { let from_events = from_events_fn(self.ty.clone()); let from_xml_builder = from_xml_builder_ty(self.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(self.ty.clone()); + quote! { + #default_() + } + } + }; + Ok(FieldBuilderPart::Nested { value: FieldTempInit { init: quote! { ::std::option::Option::None }, @@ -368,7 +384,7 @@ impl FieldDef { finalize: quote! { match #field_access { ::std::option::Option::Some(value) => value, - ::std::option::Option::None => return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()), + ::std::option::Option::None => #on_absent, } }, }) @@ -426,7 +442,7 @@ impl FieldDef { Ok(FieldIteratorPart::Text { generator }) } - FieldKind::Child => { + FieldKind::Child { default_: _ } => { let AsItemsScope { ref lifetime, .. } = scope; let as_xml_iter = as_xml_iter_fn(self.ty.clone()); diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index ff844cad..a4f148ab 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -318,7 +318,10 @@ pub(crate) enum XmlFieldMeta { }, /// `#[xml(child)` - Child, + Child { + /// The `default` flag. + default_: Flag, + }, } impl XmlFieldMeta { @@ -424,8 +427,26 @@ impl XmlFieldMeta { } /// Parse a `#[xml(child)]` meta. - fn child_from_meta(_: ParseNestedMeta<'_>) -> Result { - Ok(Self::Child) + fn child_from_meta(meta: ParseNestedMeta<'_>) -> Result { + if meta.input.peek(syn::token::Paren) { + let mut default_ = Flag::Absent; + meta.parse_nested_meta(|meta| { + 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")) + } + })?; + Ok(Self::Child { default_ }) + } else { + Ok(Self::Child { + default_: Flag::Absent, + }) + } } /// Parse [`Self`] from a nestd meta, switching on the identifier diff --git a/xso/ChangeLog b/xso/ChangeLog index c3987e59..16a564df 100644 --- a/xso/ChangeLog +++ b/xso/ChangeLog @@ -14,7 +14,8 @@ Version NEXT: All this is to avoid triggering the camel case lint on the types we generate. * Added - - Support for child elements in derive macros. + - Support for child elements in derive macros. Child elements may be + wrapped in Option. Version 0.1.2: 2024-07-26 Jonas Schäfer diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index 5fb57697..a0251051 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -139,8 +139,19 @@ assert_eq!(foo, Foo { #### `child` meta The `child` meta causes the field to be mapped to a child element of the -element. It supports no options. The field's type must implement [`FromXml`] -in order to derive `FromXml` and [`AsXml`] in order to derive `AsXml`. +element. + +| Key | Value type | Description | +| --- | --- | --- | +| `default` | flag | If present, an absent child will substitute the default value instead of raising an error. | + +The field's type must implement [`FromXml`] in order to derive `FromXml` and +[`AsXml`] in order to derive `AsXml`. + +If `default` is specified and the child 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 `AsXml`. ##### Example diff --git a/xso/src/lib.rs b/xso/src/lib.rs index 16b5cbb7..f039eb58 100644 --- a/xso/src/lib.rs +++ b/xso/src/lib.rs @@ -83,6 +83,28 @@ pub trait AsXml { fn as_xml_iter(&self) -> Result, self::error::Error>; } +/// Helper iterator to convert an `Option` to XML. +pub struct OptionAsXml(Option); + +impl<'x, T: Iterator, self::error::Error>>> Iterator for OptionAsXml { + type Item = Result, self::error::Error>; + + fn next(&mut self) -> Option { + self.0.as_mut()?.next() + } +} + +impl AsXml for Option { + type ItemIter<'x> = OptionAsXml> where T: 'x; + + fn as_xml_iter(&self) -> Result, self::error::Error> { + match self { + Some(ref value) => Ok(OptionAsXml(Some(T::as_xml_iter(value)?))), + None => Ok(OptionAsXml(None)), + } + } +} + /// Trait for a temporary object allowing to construct a struct from /// [`rxml::Event`] items. /// @@ -109,6 +131,17 @@ pub trait FromEventsBuilder { fn feed(&mut self, ev: rxml::Event) -> Result, self::error::Error>; } +/// Helper struct to construct an `Option` from XML events. +pub struct OptionBuilder(T); + +impl FromEventsBuilder for OptionBuilder { + type Output = Option; + + fn feed(&mut self, ev: rxml::Event) -> Result, self::error::Error> { + self.0.feed(ev).map(|ok| ok.map(|value| Some(value))) + } +} + /// Trait allowing to construct a struct from a stream of /// [`rxml::Event`] items. /// @@ -146,6 +179,17 @@ pub trait FromXml { ) -> Result; } +impl FromXml for Option { + type Builder = OptionBuilder; + + fn from_events( + name: rxml::QName, + attrs: rxml::AttrMap, + ) -> Result { + Ok(OptionBuilder(T::from_events(name, attrs)?)) + } +} + /// Trait allowing to convert XML text to a value. /// /// This trait is similar to [`std::str::FromStr`], however, due to