diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs
index fd9c312b..46310815 100644
--- a/parsers/src/util/macro_tests.rs
+++ b/parsers/src/util/macro_tests.rs
@@ -1181,3 +1181,56 @@ fn extract_omit_name_and_namespace_roundtrip() {
"hello world!",
)
}
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "parent")]
+struct TextExtractVec {
+ #[xml(extract(n = .., namespace = NS1, name = "child", fields(text(type_ = String))))]
+ contents: Vec,
+}
+
+#[test]
+fn text_extract_vec_positive_nonempty() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::(
+ "helloworld",
+ ) {
+ Ok(TextExtractVec { contents }) => {
+ assert_eq!(contents[0], "hello");
+ assert_eq!(contents[1], "world");
+ assert_eq!(contents.len(), 2);
+ }
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn text_extract_vec_positive_empty() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ match parse_str::("") {
+ Ok(TextExtractVec { contents }) => {
+ assert_eq!(contents.len(), 0);
+ }
+ other => panic!("unexpected result: {:?}", other),
+ }
+}
+
+#[test]
+fn text_extract_vec_roundtrip() {
+ #[allow(unused_imports)]
+ use std::{
+ option::Option::{None, Some},
+ result::Result::{Err, Ok},
+ };
+ roundtrip_full::(
+ "helloworld",
+ )
+}
diff --git a/xso-proc/src/field.rs b/xso-proc/src/field.rs
index ba0ebd3e..5a332536 100644
--- a/xso-proc/src/field.rs
+++ b/xso-proc/src/field.rs
@@ -249,7 +249,23 @@ impl FieldKind {
})
}
- XmlFieldMeta::Text { span: _, codec } => Ok(Self::Text { codec }),
+ XmlFieldMeta::Text {
+ span: _,
+ codec,
+ type_,
+ } => {
+ // This would've been taken via `XmlFieldMeta::take_type` if
+ // this field was within an extract where a `type_` is legal
+ // to have.
+ if let Some(type_) = type_ {
+ return Err(Error::new_spanned(
+ type_,
+ "specifying `type_` on fields inside structs and enum variants is redundant and not allowed."
+ ));
+ }
+
+ Ok(Self::Text { codec })
+ }
XmlFieldMeta::Child {
span: _,
@@ -280,12 +296,13 @@ impl FieldKind {
XmlFieldMeta::Extract {
span,
qname: QNameRef { namespace, name },
+ amount,
fields,
} => {
let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone());
let xml_name = default_name(span, name, field_ident)?;
- let field = {
+ let mut field = {
let mut fields = fields.into_iter();
let Some(field) = fields.next() else {
return Err(Error::new(
@@ -304,13 +321,28 @@ impl FieldKind {
field
};
+ let amount = amount.unwrap_or(AmountConstraint::FixedSingle(Span::call_site()));
+ let field_ty = match field.take_type() {
+ Some(v) => v,
+ None => match amount {
+ // Only allow inferrence for single values: inferrence
+ // for collections will always be wrong.
+ AmountConstraint::FixedSingle(_) => field_ty.clone(),
+ _ => {
+ return Err(Error::new(
+ field.span(),
+ "extracted field must specify a type explicitly when extracting into a collection."
+ ));
+ }
+ },
+ };
let parts = Compound::from_field_defs(
- [FieldDef::from_extract(field, 0, field_ty, &xml_namespace)].into_iter(),
+ [FieldDef::from_extract(field, 0, &field_ty, &xml_namespace)].into_iter(),
)?;
Ok(Self::Child {
default_: Flag::Absent,
- amount: AmountConstraint::FixedSingle(Span::call_site()),
+ amount,
extract: Some(ExtractDef {
xml_namespace,
xml_name,
diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs
index ac9541b6..05867197 100644
--- a/xso-proc/src/meta.rs
+++ b/xso-proc/src/meta.rs
@@ -645,6 +645,9 @@ pub(crate) enum XmlFieldMeta {
/// The path to the optional codec type.
codec: Option,
+
+ /// An explicit type override, only usable within extracts.
+ type_: Option,
},
/// `#[xml(child)`
@@ -671,6 +674,9 @@ pub(crate) enum XmlFieldMeta {
/// The namespace/name keys.
qname: QNameRef,
+ /// The `n` flag.
+ amount: Option,
+
/// The `fields` nested meta.
fields: Vec,
},
@@ -751,10 +757,12 @@ impl XmlFieldMeta {
}
Ok(Self::Text {
span: meta.path.span(),
+ type_: None,
codec: Some(codec),
})
} else if meta.input.peek(syn::token::Paren) {
let mut codec: Option = None;
+ let mut type_: Option = None;
meta.parse_nested_meta(|meta| {
if meta.path.is_ident("codec") {
if codec.is_some() {
@@ -773,17 +781,25 @@ impl XmlFieldMeta {
}
codec = Some(new_codec);
Ok(())
+ } else if meta.path.is_ident("type_") {
+ if type_.is_some() {
+ return Err(Error::new_spanned(meta.path, "duplicate `type_` key"));
+ }
+ type_ = Some(meta.value()?.parse()?);
+ Ok(())
} else {
Err(Error::new_spanned(meta.path, "unsupported key"))
}
})?;
Ok(Self::Text {
span: meta.path.span(),
+ type_,
codec,
})
} else {
Ok(Self::Text {
span: meta.path.span(),
+ type_: None,
codec: None,
})
}
@@ -829,6 +845,7 @@ impl XmlFieldMeta {
fn extract_from_meta(meta: ParseNestedMeta<'_>) -> Result {
let mut qname = QNameRef::default();
let mut fields = None;
+ let mut amount = None;
meta.parse_nested_meta(|meta| {
if meta.path.is_ident("fields") {
if let Some((fields_span, _)) = fields.as_ref() {
@@ -843,6 +860,12 @@ impl XmlFieldMeta {
})?;
fields = Some((meta.path.span(), new_fields));
Ok(())
+ } else if meta.path.is_ident("n") {
+ if amount.is_some() {
+ return Err(Error::new_spanned(meta.path, "duplicate `n` key"));
+ }
+ amount = Some(meta.value()?.parse()?);
+ Ok(())
} else {
match qname.parse_incremental_from_meta(meta)? {
None => Ok(()),
@@ -855,6 +878,7 @@ impl XmlFieldMeta {
span: meta.path.span(),
qname,
fields,
+ amount,
})
}
@@ -952,4 +976,12 @@ impl XmlFieldMeta {
Self::Extract { ref span, .. } => *span,
}
}
+
+ /// Extract an explicit type specification if it exists.
+ pub(crate) fn take_type(&mut self) -> Option {
+ match self {
+ Self::Text { ref mut type_, .. } => type_.take(),
+ _ => None,
+ }
+ }
}
diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md
index 3f8f24be..3a3e90e3 100644
--- a/xso/src/from_xml_doc.md
+++ b/xso/src/from_xml_doc.md
@@ -308,6 +308,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`. |
+| `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. |
If the `name` key contains a namespace prefix, it must be one of the prefixes
@@ -322,6 +323,14 @@ and the `extract` meta is being used on a named field, that field's name is
used. If `name` is omitted and `extract` is not used on a named field, an
error is emitted.
+When parsing a single child element (i.e. `n = 1` or no `n` value set at all),
+the extracted field's type is set to be the same type as the field on which
+the extract is declared, unless overridden in the extracted field's meta.
+
+When parsing a collection (with `n = ..`), the extracted fields within
+`fields()` must all have type specifications. Not all fields kinds support
+that.
+
The sequence of field meta inside `fields` can be thought of as a nameless
tuple-style struct. The macro generates serialisation/deserialisation code
for that nameless tuple-style struct and uses it to serialise/deserialise
@@ -367,6 +376,7 @@ element.
| Key | Value type | Description |
| --- | --- | --- |
| `codec` | *expression* | Optional [`TextCodec`] implementation which is used to encode or decode the field. |
+| `type_` | *type* | Optional explicit type specification. Only allowed within `#[xml(extract(fields(..)))]`. |
If `codec` is given, the given `codec` value must implement
[`TextCodec`][`TextCodec`] where `T` is the type of the field.
@@ -374,6 +384,10 @@ If `codec` is given, the given `codec` value must implement
If `codec` is *not* given, the field's type must implement [`FromXmlText`] for
`FromXml` and for `AsXml`, the field's type must implement [`AsXmlText`].
+If `type_` is specified and the `text` meta is used within an
+`#[xml(extract(fields(..)))]` meta, the specified type is used instead of the
+field type on which the `extract` is declared.
+
The `text` meta also supports a shorthand syntax, `#[xml(text = ..)]`, where
the value is treated as the value for the `codec` key (with optional prefix as
described above, and unnamespaced otherwise).