xso: add support for extracting into collections
This commit is contained in:
parent
5590d707fb
commit
8732ca9a28
4 changed files with 135 additions and 4 deletions
|
|
@ -1181,3 +1181,56 @@ fn extract_omit_name_and_namespace_roundtrip() {
|
||||||
"<parent xmlns='urn:example:ns1'><contents>hello world!</contents></parent>",
|
"<parent xmlns='urn:example:ns1'><contents>hello world!</contents></parent>",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_extract_vec_positive_nonempty() {
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use std::{
|
||||||
|
option::Option::{None, Some},
|
||||||
|
result::Result::{Err, Ok},
|
||||||
|
};
|
||||||
|
match parse_str::<TextExtractVec>(
|
||||||
|
"<parent xmlns='urn:example:ns1'><child>hello</child><child>world</child></parent>",
|
||||||
|
) {
|
||||||
|
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::<TextExtractVec>("<parent xmlns='urn:example:ns1'/>") {
|
||||||
|
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::<TextExtractVec>(
|
||||||
|
"<parent xmlns='urn:example:ns1'><child>hello</child><child>world</child></parent>",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
XmlFieldMeta::Child {
|
||||||
span: _,
|
span: _,
|
||||||
|
|
@ -280,12 +296,13 @@ impl FieldKind {
|
||||||
XmlFieldMeta::Extract {
|
XmlFieldMeta::Extract {
|
||||||
span,
|
span,
|
||||||
qname: QNameRef { namespace, name },
|
qname: QNameRef { namespace, name },
|
||||||
|
amount,
|
||||||
fields,
|
fields,
|
||||||
} => {
|
} => {
|
||||||
let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone());
|
let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone());
|
||||||
let xml_name = default_name(span, name, field_ident)?;
|
let xml_name = default_name(span, name, field_ident)?;
|
||||||
|
|
||||||
let field = {
|
let mut field = {
|
||||||
let mut fields = fields.into_iter();
|
let mut fields = fields.into_iter();
|
||||||
let Some(field) = fields.next() else {
|
let Some(field) = fields.next() else {
|
||||||
return Err(Error::new(
|
return Err(Error::new(
|
||||||
|
|
@ -304,13 +321,28 @@ impl FieldKind {
|
||||||
field
|
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(
|
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 {
|
Ok(Self::Child {
|
||||||
default_: Flag::Absent,
|
default_: Flag::Absent,
|
||||||
amount: AmountConstraint::FixedSingle(Span::call_site()),
|
amount,
|
||||||
extract: Some(ExtractDef {
|
extract: Some(ExtractDef {
|
||||||
xml_namespace,
|
xml_namespace,
|
||||||
xml_name,
|
xml_name,
|
||||||
|
|
|
||||||
|
|
@ -645,6 +645,9 @@ pub(crate) enum XmlFieldMeta {
|
||||||
|
|
||||||
/// The path to the optional codec type.
|
/// The path to the optional codec type.
|
||||||
codec: Option<Expr>,
|
codec: Option<Expr>,
|
||||||
|
|
||||||
|
/// An explicit type override, only usable within extracts.
|
||||||
|
type_: Option<Type>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// `#[xml(child)`
|
/// `#[xml(child)`
|
||||||
|
|
@ -671,6 +674,9 @@ pub(crate) enum XmlFieldMeta {
|
||||||
/// The namespace/name keys.
|
/// The namespace/name keys.
|
||||||
qname: QNameRef,
|
qname: QNameRef,
|
||||||
|
|
||||||
|
/// The `n` flag.
|
||||||
|
amount: Option<AmountConstraint>,
|
||||||
|
|
||||||
/// The `fields` nested meta.
|
/// The `fields` nested meta.
|
||||||
fields: Vec<XmlFieldMeta>,
|
fields: Vec<XmlFieldMeta>,
|
||||||
},
|
},
|
||||||
|
|
@ -751,10 +757,12 @@ impl XmlFieldMeta {
|
||||||
}
|
}
|
||||||
Ok(Self::Text {
|
Ok(Self::Text {
|
||||||
span: meta.path.span(),
|
span: meta.path.span(),
|
||||||
|
type_: None,
|
||||||
codec: Some(codec),
|
codec: Some(codec),
|
||||||
})
|
})
|
||||||
} else if meta.input.peek(syn::token::Paren) {
|
} else if meta.input.peek(syn::token::Paren) {
|
||||||
let mut codec: Option<Expr> = None;
|
let mut codec: Option<Expr> = None;
|
||||||
|
let mut type_: Option<Type> = None;
|
||||||
meta.parse_nested_meta(|meta| {
|
meta.parse_nested_meta(|meta| {
|
||||||
if meta.path.is_ident("codec") {
|
if meta.path.is_ident("codec") {
|
||||||
if codec.is_some() {
|
if codec.is_some() {
|
||||||
|
|
@ -773,17 +781,25 @@ impl XmlFieldMeta {
|
||||||
}
|
}
|
||||||
codec = Some(new_codec);
|
codec = Some(new_codec);
|
||||||
Ok(())
|
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 {
|
} else {
|
||||||
Err(Error::new_spanned(meta.path, "unsupported key"))
|
Err(Error::new_spanned(meta.path, "unsupported key"))
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
Ok(Self::Text {
|
Ok(Self::Text {
|
||||||
span: meta.path.span(),
|
span: meta.path.span(),
|
||||||
|
type_,
|
||||||
codec,
|
codec,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(Self::Text {
|
Ok(Self::Text {
|
||||||
span: meta.path.span(),
|
span: meta.path.span(),
|
||||||
|
type_: None,
|
||||||
codec: None,
|
codec: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -829,6 +845,7 @@ impl XmlFieldMeta {
|
||||||
fn extract_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
fn extract_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
||||||
let mut qname = QNameRef::default();
|
let mut qname = QNameRef::default();
|
||||||
let mut fields = None;
|
let mut fields = None;
|
||||||
|
let mut amount = None;
|
||||||
meta.parse_nested_meta(|meta| {
|
meta.parse_nested_meta(|meta| {
|
||||||
if meta.path.is_ident("fields") {
|
if meta.path.is_ident("fields") {
|
||||||
if let Some((fields_span, _)) = fields.as_ref() {
|
if let Some((fields_span, _)) = fields.as_ref() {
|
||||||
|
|
@ -843,6 +860,12 @@ impl XmlFieldMeta {
|
||||||
})?;
|
})?;
|
||||||
fields = Some((meta.path.span(), new_fields));
|
fields = Some((meta.path.span(), new_fields));
|
||||||
Ok(())
|
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 {
|
} else {
|
||||||
match qname.parse_incremental_from_meta(meta)? {
|
match qname.parse_incremental_from_meta(meta)? {
|
||||||
None => Ok(()),
|
None => Ok(()),
|
||||||
|
|
@ -855,6 +878,7 @@ impl XmlFieldMeta {
|
||||||
span: meta.path.span(),
|
span: meta.path.span(),
|
||||||
qname,
|
qname,
|
||||||
fields,
|
fields,
|
||||||
|
amount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -952,4 +976,12 @@ impl XmlFieldMeta {
|
||||||
Self::Extract { ref span, .. } => *span,
|
Self::Extract { ref span, .. } => *span,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract an explicit type specification if it exists.
|
||||||
|
pub(crate) fn take_type(&mut self) -> Option<Type> {
|
||||||
|
match self {
|
||||||
|
Self::Text { ref mut type_, .. } => type_.take(),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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. |
|
| `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`. |
|
| `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. |
|
| `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
|
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
|
used. If `name` is omitted and `extract` is not used on a named field, an
|
||||||
error is emitted.
|
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
|
The sequence of field meta inside `fields` can be thought of as a nameless
|
||||||
tuple-style struct. The macro generates serialisation/deserialisation code
|
tuple-style struct. The macro generates serialisation/deserialisation code
|
||||||
for that nameless tuple-style struct and uses it to serialise/deserialise
|
for that nameless tuple-style struct and uses it to serialise/deserialise
|
||||||
|
|
@ -367,6 +376,7 @@ element.
|
||||||
| Key | Value type | Description |
|
| Key | Value type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `codec` | *expression* | Optional [`TextCodec`] implementation which is used to encode or decode the field. |
|
| `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
|
If `codec` is given, the given `codec` value must implement
|
||||||
[`TextCodec<T>`][`TextCodec`] where `T` is the type of the field.
|
[`TextCodec<T>`][`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
|
If `codec` is *not* given, the field's type must implement [`FromXmlText`] for
|
||||||
`FromXml` and for `AsXml`, the field's type must implement [`AsXmlText`].
|
`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 `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
|
the value is treated as the value for the `codec` key (with optional prefix as
|
||||||
described above, and unnamespaced otherwise).
|
described above, and unnamespaced otherwise).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue