Detailed changes
@@ -1287,3 +1287,236 @@ fn text_extract_attribute_vec_roundtrip() {
"<parent xmlns='urn:example:ns1'><child attr='hello'/><child attr='world'/></parent>",
)
}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct TextOptionalExtract {
+ #[xml(extract(namespace = NS1, name = "child", default, fields(text(type_ = String))))]
+ contents: ::std::option::Option<String>,
+}
+
+#[test]
+fn text_optional_extract_positive_present() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<TextOptionalExtract>(
+ "<parent xmlns='urn:example:ns1'><child>hello world</child></parent>",
+ ) {
+ Ok(TextOptionalExtract {
+ contents: Some(contents),
+ }) => {
+ assert_eq!(contents, "hello world");
+ }
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn text_optional_extract_positive_absent_child() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<TextOptionalExtract>("<parent xmlns='urn:example:ns1'/>") {
+ Ok(TextOptionalExtract { contents: None }) => (),
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn text_optional_extract_roundtrip_present() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<TextOptionalExtract>(
+ "<parent xmlns='urn:example:ns1'><child>hello world!</child></parent>",
+ )
+}
+
+#[test]
+fn text_optional_extract_roundtrip_absent() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<TextOptionalExtract>("<parent xmlns='urn:example:ns1'/>")
+}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct OptionalAttributeOptionalExtract {
+ #[xml(extract(namespace = NS1, name = "child", default, fields(attribute(name = "foo", default))))]
+ contents: ::std::option::Option<String>,
+}
+
+#[test]
+fn optional_attribute_optional_extract_positive_present() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<OptionalAttributeOptionalExtract>(
+ "<parent xmlns='urn:example:ns1'><child foo='hello world'/></parent>",
+ ) {
+ Ok(OptionalAttributeOptionalExtract {
+ contents: Some(contents),
+ }) => {
+ assert_eq!(contents, "hello world");
+ }
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn optional_attribute_optional_extract_positive_absent_attribute() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<OptionalAttributeOptionalExtract>(
+ "<parent xmlns='urn:example:ns1'><child/></parent>",
+ ) {
+ Ok(OptionalAttributeOptionalExtract { contents: None }) => (),
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn optional_attribute_optional_extract_positive_absent_element() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<OptionalAttributeOptionalExtract>("<parent xmlns='urn:example:ns1'/>") {
+ Ok(OptionalAttributeOptionalExtract { contents: None }) => (),
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn optional_attribute_optional_extract_roundtrip_present() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<OptionalAttributeOptionalExtract>(
+ "<parent xmlns='urn:example:ns1'><child foo='hello world'/></parent>",
+ )
+}
+
+#[test]
+fn optional_attribute_optional_extract_roundtrip_absent_attribute() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<OptionalAttributeOptionalExtract>(
+ "<parent xmlns='urn:example:ns1'><child/></parent>",
+ )
+}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct OptionalAttributeOptionalExtractDoubleOption {
+ #[xml(extract(namespace = NS1, name = "child", default, fields(attribute(name = "foo", type_ = ::std::option::Option<String>, default))))]
+ contents: ::std::option::Option<::std::option::Option<String>>,
+}
+
+#[test]
+fn optional_attribute_optional_extract_double_option_positive_present() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<OptionalAttributeOptionalExtractDoubleOption>(
+ "<parent xmlns='urn:example:ns1'><child foo='hello world'/></parent>",
+ ) {
+ Ok(OptionalAttributeOptionalExtractDoubleOption {
+ contents: Some(Some(contents)),
+ }) => {
+ assert_eq!(contents, "hello world");
+ }
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn optional_attribute_optional_extract_double_option_positive_absent_attribute() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<OptionalAttributeOptionalExtractDoubleOption>(
+ "<parent xmlns='urn:example:ns1'><child/></parent>",
+ ) {
+ Ok(OptionalAttributeOptionalExtractDoubleOption {
+ contents: Some(None),
+ }) => (),
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn optional_attribute_optional_extract_double_option_positive_absent_element() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::<OptionalAttributeOptionalExtractDoubleOption>(
+ "<parent xmlns='urn:example:ns1'/>",
+ ) {
+ Ok(OptionalAttributeOptionalExtractDoubleOption { contents: None }) => (),
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn optional_attribute_optional_extract_double_option_roundtrip_present() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<OptionalAttributeOptionalExtractDoubleOption>(
+ "<parent xmlns='urn:example:ns1'><child foo='hello world'/></parent>",
+ )
+}
+
+#[test]
+fn optional_attribute_optional_extract_double_option_roundtrip_absent_attribute() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<OptionalAttributeOptionalExtractDoubleOption>(
+ "<parent xmlns='urn:example:ns1'><child/></parent>",
+ )
+}
+
+#[test]
+fn optional_attribute_optional_extract_double_option_roundtrip_absent_child() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::<OptionalAttributeOptionalExtractDoubleOption>(
+ "<parent xmlns='urn:example:ns1'/>",
+ )
+}
@@ -15,7 +15,7 @@ use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit}
use crate::meta::NamespaceRef;
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
-use crate::types::{feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty};
+use crate::types::{feed_fn, namespace_ty, ncnamestr_cow_ty, phantom_lifetime_ty, ref_ty};
/// A struct or enum variant's contents.
pub(crate) struct Compound {
@@ -517,4 +517,27 @@ impl Compound {
},
})
}
+
+ /// Construct a tuple type with this compound's field's types in the same
+ /// order as they appear in the compound.
+ pub(crate) fn to_tuple_ty(&self) -> TypeTuple {
+ TypeTuple {
+ paren_token: token::Paren::default(),
+ elems: self.fields.iter().map(|x| x.ty().clone()).collect(),
+ }
+ }
+
+ /// Construct a tuple type with references to this compound's field's
+ /// types in the same order as they appear in the compound, with the given
+ /// lifetime.
+ pub(crate) fn to_ref_tuple_ty(&self, lifetime: &Lifetime) -> TypeTuple {
+ TypeTuple {
+ paren_token: token::Paren::default(),
+ elems: self
+ .fields
+ .iter()
+ .map(|x| ref_ty(x.ty().clone(), lifetime.clone()))
+ .collect(),
+ }
+ }
}
@@ -19,8 +19,8 @@ use crate::scope::{AsItemsScope, FromEventsScope};
use crate::types::{
as_optional_xml_text_fn, as_xml_iter_fn, as_xml_text_fn, default_fn, extend_fn, from_events_fn,
from_xml_builder_ty, from_xml_text_fn, into_iterator_into_iter_fn, into_iterator_item_ty,
- into_iterator_iter_ty, item_iter_ty, option_ty, ref_ty, string_ty, text_codec_decode_fn,
- text_codec_encode_fn, ty_from_ident,
+ into_iterator_iter_ty, item_iter_ty, option_as_xml_ty, option_ty, ref_ty, string_ty,
+ text_codec_decode_fn, text_codec_encode_fn, ty_from_ident,
};
/// Code slices necessary for declaring and initializing a temporary variable
@@ -306,6 +306,7 @@ impl FieldKind {
XmlFieldMeta::Extract {
span,
+ default_,
qname: QNameRef { namespace, name },
amount,
fields,
@@ -352,7 +353,7 @@ impl FieldKind {
)?;
Ok(Self::Child {
- default_: Flag::Absent,
+ default_,
amount,
extract: Some(ExtractDef {
xml_namespace,
@@ -565,12 +566,7 @@ impl FieldDef {
&Visibility::Inherited,
&from_xml_builder_ty_ident,
&state_ty_ident,
- &Type::Tuple(TypeTuple {
- paren_token: token::Paren::default(),
- elems: [
- element_ty.clone(),
- ].into_iter().collect(),
- })
+ &parts.to_tuple_ty().into(),
)?;
let from_xml_builder_ty =
ty_from_ident(from_xml_builder_ty_ident.clone()).into();
@@ -580,7 +576,31 @@ impl FieldDef {
(
extra_defs,
matcher,
- quote! { #substate_result.0 },
+ // This little ".into()" here goes a long way. It
+ // relies on one of the most underrated trait
+ // implementations in the standard library:
+ // `impl From<T> for Option<T>`, which creates a
+ // `Some(_)` from a `T`. Why is it so great?
+ // Because there is also `impl From<Option<T>> for
+ // Option<T>` (obviously), which is just a move.
+ // So even without knowing the exact type of the
+ // substate result and the field, we can make an
+ // "downcast" to `Option<T>` if the field is of
+ // type `Option<T>`, and it does the right thing
+ // no matter whether the extracted field is of
+ // type `Option<T>` or `T`.
+ //
+ // And then, type inferrence does the rest: There
+ // is ambiguity there, of course, if we call
+ // `.into()` on a value of type `Option<T>`:
+ // Should Rust wrap it into another layer of
+ // `Option`, or should it just move the value? The
+ // answer lies in the type constraint imposed by
+ // the place the value is *used*, which is
+ // strictly bound by the field's type (so there
+ // is, in fact, no ambiguity). So this works all
+ // kinds of magic.
+ quote! { #substate_result.0.into() },
from_xml_builder_ty,
)
}
@@ -736,7 +756,7 @@ impl FieldDef {
}
};
- let (extra_defs, fetch, as_xml_iter, iter_ty) = match extract {
+ let (extra_defs, init, iter_ty) = match extract {
Some(ExtractDef {
ref xml_namespace,
ref xml_name,
@@ -776,21 +796,25 @@ impl FieldDef {
.compile()
.render(
&Visibility::Inherited,
- &Type::Tuple(TypeTuple {
- paren_token: token::Paren::default(),
- elems: [ref_ty(item_ty.clone(), lifetime.clone())]
- .into_iter()
- .collect(),
- }),
+ &parts.to_ref_tuple_ty(lifetime).into(),
&state_ty_ident,
lifetime,
&item_iter_ty,
)?;
+ let item_iter_ty = option_as_xml_ty(item_iter_ty);
(
extra_defs,
- quote! { (&#bound_name,) },
- quote! { #item_iter_ty_ident::new },
+ // Again we exploit the extreme usefulness of the
+ // `impl From<T> for Option<T>`. We already wrote
+ // extensively about that in the FromXml
+ // implementation corresponding to this code
+ // above, and we will not repeat it here.
+ quote! {
+ ::xso::OptionAsXml::new(::core::option::Option::from(#bound_name).map(|#bound_name| {
+ #item_iter_ty_ident::new((#bound_name,))
+ }).transpose()?)
+ },
item_iter_ty,
)
}
@@ -800,8 +824,7 @@ impl FieldDef {
(
TokenStream::default(),
- quote! { #bound_name },
- quote! { #as_xml_iter },
+ quote! { #as_xml_iter(#bound_name)? },
item_iter,
)
}
@@ -810,12 +833,7 @@ impl FieldDef {
match amount {
AmountConstraint::FixedSingle(_) => Ok(FieldIteratorPart::Content {
extra_defs,
- value: FieldTempInit {
- init: quote! {
- #as_xml_iter(#fetch)?
- },
- ty: iter_ty,
- },
+ value: FieldTempInit { init, ty: iter_ty },
generator: quote! {
#bound_name.next().transpose()
},
@@ -853,7 +871,10 @@ impl FieldDef {
}
}
if let ::core::option::Option::Some(item) = #bound_name.0.next() {
- #bound_name.1 = ::core::option::Option::Some(#as_xml_iter({ let #bound_name = item; #fetch })?)
+ #bound_name.1 = ::core::option::Option::Some({
+ let #bound_name = item;
+ #init
+ });
} else {
break ::core::result::Result::Ok(::core::option::Option::None)
}
@@ -680,6 +680,9 @@ pub(crate) enum XmlFieldMeta {
/// The `n` flag.
amount: Option<AmountConstraint>,
+ /// The `default` flag.
+ default_: Flag,
+
/// The `fields` nested meta.
fields: Vec<XmlFieldMeta>,
},
@@ -859,8 +862,15 @@ impl XmlFieldMeta {
let mut qname = QNameRef::default();
let mut fields = None;
let mut amount = None;
+ let mut default_ = Flag::Absent;
meta.parse_nested_meta(|meta| {
- if meta.path.is_ident("fields") {
+ 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("fields") {
if let Some((fields_span, _)) = fields.as_ref() {
let mut error = Error::new_spanned(meta.path, "duplicate `fields` meta");
error.combine(Error::new(*fields_span, "previous `fields` meta was here"));
@@ -889,6 +899,7 @@ impl XmlFieldMeta {
let fields = fields.map(|(_, x)| x).unwrap_or_else(Vec::new);
Ok(Self::Extract {
span: meta.path.span(),
+ default_,
qname,
fields,
amount,
@@ -753,3 +753,33 @@ pub(crate) fn ty_from_ident(ident: Ident) -> TypePath {
path: ident.into(),
}
}
+
+/// Construct a [`syn::Type`] referring to `xso::OptionAsXml<#inner_ty>`.
+pub(crate) fn option_as_xml_ty(inner_ty: Type) -> Type {
+ let span = inner_ty.span();
+ Type::Path(TypePath {
+ qself: None,
+ path: Path {
+ leading_colon: Some(syn::token::PathSep {
+ spans: [span, span],
+ }),
+ segments: [
+ PathSegment {
+ ident: Ident::new("xso", span),
+ arguments: PathArguments::None,
+ },
+ PathSegment {
+ ident: Ident::new("OptionAsXml", span),
+ arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments {
+ colon2_token: None,
+ lt_token: token::Lt { spans: [span] },
+ args: [GenericArgument::Type(inner_ty)].into_iter().collect(),
+ gt_token: token::Gt { spans: [span] },
+ }),
+ },
+ ]
+ .into_iter()
+ .collect(),
+ },
+ })
+}
@@ -251,6 +251,11 @@ implement the `Default` trait for a `FromXml` derivation. `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>`).
+
##### Example
```rust
@@ -313,6 +318,7 @@ The following keys can be used inside the `#[xml(extract(..))]` meta:
| --- | --- | --- |
| `namespace` | *string literal* or *path* | The XML namespace of the child element. |
| `name` | *string literal* or *path* | The XML name of the child element. If it is a *path*, it must point at a `&'static NcNameStr`. |
+| `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`. |
| `fields` | *nested* | A list of [field meta](#field-meta) which describe the contents of the child element. |
@@ -341,8 +347,30 @@ tuple-style struct. The macro generates serialisation/deserialisation code
for that nameless tuple-style struct and uses it to serialise/deserialise
the field.
+If `default` is specified and the child is absent in the source, the value
+is generated using [`core::default::Default`], requiring the field type to
+implement the `Default` trait for a `FromXml` derivation. `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.
+
+Mixing `default` on the `#[xml(extract)]` itself with `default` on the
+extracted inner fields creates non-roundtrip-safe parsing, unless you also
+use twice-nested [`core::option::Option`] types. That means that when
+deserialising a piece of XML and reserialising it without changing the
+contents of the struct in Rust, the resulting XML may not match the input.
+This is because to the serialiser, if only one layer of
+[`core::option::Option`] is used, it is not possible to distinguish which of
+the two layers were defaulted. The exact behaviour on serialisation in such a
+situation is *not guaranteed* and may change between versions of the `xso`
+crate, its dependencies, the standard library, or even rustc itself.
+
+Using `default` with a type other than `Option<T>` will cause the
+serialisation to mismatch the deserialisation, too (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>`).
+
**Note:** Currently, only a single field can be extracted. This restriction
-will be lifted in the future. Collections are not supported yet, either.
+will be lifted in the future.
Using `extract` instead of `child` combined with a specific struct declaration
comes with trade-offs. On the one hand, using `extract` gives you flexibility
@@ -86,6 +86,17 @@ pub trait AsXml {
/// Helper iterator to convert an `Option<T>` to XML.
pub struct OptionAsXml<T: Iterator>(Option<T>);
+impl<T: Iterator> OptionAsXml<T> {
+ /// Construct a new iterator, wrapping the given iterator.
+ ///
+ /// If `inner` is `None`, this iterator terminates immediately. Otherwise,
+ /// it yields the elements yielded by `inner` until `inner` finishes,
+ /// after which this iterator completes, too.
+ pub fn new(inner: Option<T>) -> Self {
+ Self(inner)
+ }
+}
+
impl<'x, T: Iterator<Item = Result<Item<'x>, self::error::Error>>> Iterator for OptionAsXml<T> {
type Item = Result<Item<'x>, self::error::Error>;