1
0
mirror of https://gitlab.com/xmpp-rs/xmpp-rs.git synced 2024-06-18 13:45:57 +02:00

parsers-macros: implement support for extended child destructuring

This commit is contained in:
Jonas Schäfer 2024-04-01 18:11:31 +02:00
parent c0ea48f22f
commit e8e6b12fd2
5 changed files with 174 additions and 23 deletions

View File

@ -759,3 +759,35 @@ impl UnknownAttributePolicy {
}
}
}
/// Trait to support destructuring of child structs beyond what the
/// `extract(..)` attribute can deliver.
///
/// This trait can only be sensibly implemented on types which implement both
/// `FromXml` and `IntoXml`. However, as there may be corner cases where only
/// one of these other traits is needed, they're not strictly included in the
/// trait bounds.
///
/// When used as value for `codec = ..` inside a `#[xml(child(..))]` or
/// `#[xml(children(..))]` field attribute, the field is destructured using
/// the trait implementations of `FromXml` / `IntoXml` and then converted
/// to the actual field's type by invoking the `ElementCodec<T>` methods, with
/// `T` being the field type.
pub trait ElementCodec<T> {
/// Transform the destructured value further toward the field type.
fn decode(value: Self) -> T;
/// Transform the field type back to something which can be structured
/// into XML.
fn encode(value: T) -> Self;
}
impl<T> ElementCodec<T> for T {
fn decode(value: Self) -> Self {
value
}
fn encode(value: Self) -> Self {
value
}
}

View File

@ -252,6 +252,11 @@ pub(crate) struct ChildField {
/// an immutable reference to the field's value and must return a boolean.
/// If that boolean is true, the child will not be emitted.
skip_if: Option<Path>,
/// If set, it must point to a type. The `FromXml`/`IntoXml`
/// implementations of that type will be used instead, and the type must
/// implement `ElementCodec<T>`, where `T` is the type of the field.
codec: Option<Path>,
}
impl ChildField {
@ -283,6 +288,7 @@ impl ChildField {
extract: Vec<Box<XmlFieldMeta>>,
default_: FlagOr<Path>,
skip_if: Option<Path>,
codec: Option<Path>,
field_type: &Type,
) -> Result<Self> {
if extract.len() > 0 {
@ -308,6 +314,12 @@ impl ChildField {
"name must be specified on extracted fields",
));
};
if let Some(codec) = codec {
return Err(Error::new_spanned(
codec,
"codec = .. cannot be combined with extract(..)",
));
}
let single_extract_type = match mode {
ChildMode::Single => {
if extract.len() > 1 {
@ -332,6 +344,7 @@ impl ChildField {
skip_if,
default_,
super_namespace: Flag::Absent,
codec: None,
})
} else {
let super_namespace = match namespace {
@ -356,6 +369,7 @@ impl ChildField {
default_,
skip_if,
super_namespace,
codec,
})
}
}
@ -450,14 +464,23 @@ impl ChildField {
::std::cmp::PartialEq::eq(&#container_namespace_expr, residual.ns().as_str())
},
};
let codec_ty = match self.codec {
Some(ty) => Type::Path(TypePath {
qself: None,
path: ty,
}),
None => ty.clone(),
};
let field_ty = ty;
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname: Option<#ty> = None;
let mut #tempname: Option<#field_ty> = None;
},
childiter: quote! {
let mut residual = if #ns_test {
match <#ty as ::xmpp_parsers_core::FromXml>::from_tree(residual) {
match <#codec_ty as ::xmpp_parsers_core::FromXml>::from_tree(residual) {
Ok(v) => {
let v = <#codec_ty as ::xmpp_parsers_core::ElementCodec::<#field_ty>>::decode(v);
if #tempname.is_some() {
return Err(::xmpp_parsers_core::error::Error::ParseError(#duperr));
}
@ -474,7 +497,7 @@ impl ChildField {
value: quote! {
if let Some(v) = #tempname {
v
} else if let Some(v) = <#ty as ::xmpp_parsers_core::FromXml>::absent() {
} else if let Some(v) = <#codec_ty as ::xmpp_parsers_core::FromXml>::absent().map(<#codec_ty as ::xmpp_parsers_core::ElementCodec::<#field_ty>>::decode) {
v
} else {
#on_missing
@ -524,13 +547,21 @@ impl ChildField {
<#ty as IntoIterator>::Item
})
.expect("failed to construct item type");
let codec_ty = match self.codec {
Some(ty) => Type::Path(TypePath {
qself: None,
path: ty,
}),
None => item_ty.clone(),
};
Ok(FieldParsePart {
tempinit: quote! {
let mut #tempname = <#ty as std::default::Default>::default();
},
childiter: quote! {
let mut residual = match <#item_ty as ::xmpp_parsers_core::FromXml>::from_tree(residual) {
let mut residual = match <#codec_ty as ::xmpp_parsers_core::FromXml>::from_tree(residual) {
Ok(item) => {
let item = <#codec_ty as ::xmpp_parsers_core::ElementCodec::<#item_ty>>::decode(item);
<#ty as ::std::iter::Extend<#item_ty>>::extend(&mut #tempname, [item]);
continue;
},
@ -625,15 +656,25 @@ impl ChildField {
}
})
}
None => Ok(quote! {
{
let #temp_ident = #ident;
match #skip_map.and_then(|#temp_ident| <#ty as ::xmpp_parsers_core::IntoXml>::into_tree(#temp_ident)) {
Some(#temp_ident) => builder.append(::xmpp_parsers_core::exports::minidom::Node::Element(#temp_ident)),
None => builder,
None => {
let codec_ty = match self.codec {
Some(ty) => Type::Path(TypePath {
qself: None,
path: ty,
}),
None => ty.clone(),
};
let field_ty = ty;
Ok(quote! {
{
let #temp_ident = #ident;
match #skip_map.and_then(|#temp_ident| <#codec_ty as ::xmpp_parsers_core::IntoXml>::into_tree(<#codec_ty as ::xmpp_parsers_core::ElementCodec::<#field_ty>>::encode(#temp_ident))) {
Some(#temp_ident) => builder.append(::xmpp_parsers_core::exports::minidom::Node::Element(#temp_ident)),
None => builder,
}
}
}
}),
})
}
},
ChildMode::Collection => match self.extract {
Some(extract) => {
@ -657,11 +698,24 @@ impl ChildField {
)
})
}
None => Ok(quote! {
builder.append_all(#ident.into_iter().filter_map(|#temp_ident| {
#skip_map.and_then(::xmpp_parsers_core::IntoXml::into_tree).map(|el| ::xmpp_parsers_core::exports::minidom::Node::Element(el))
}))
}),
None => {
let item_ty: Type = syn::parse2(quote! {
<#ty as IntoIterator>::Item
})
.expect("failed to construct item type");
let codec_ty = match self.codec {
Some(ty) => Type::Path(TypePath {
qself: None,
path: ty,
}),
None => item_ty.clone(),
};
Ok(quote! {
builder.append_all(#ident.into_iter().filter_map(|#temp_ident| {
#skip_map.map(<#codec_ty as ::xmpp_parsers_core::ElementCodec::<#item_ty>>::encode).and_then(::xmpp_parsers_core::IntoXml::into_tree).map(|el| ::xmpp_parsers_core::exports::minidom::Node::Element(el))
}))
})
}
},
}
}

View File

@ -196,8 +196,9 @@ impl FieldKind {
extract,
default_,
skip_if,
codec,
} => Ok(FieldKind::Child(ChildField::new(
span, mode, namespace, name, extract, default_, skip_if, field_ty,
span, mode, namespace, name, extract, default_, skip_if, codec, field_ty,
)?)),
XmlFieldMeta::Text { codec, ty } => {
if let Some(ty) = ty {

View File

@ -824,6 +824,9 @@ pub(crate) enum XmlFieldMeta {
/// Contents of the `skip_if = ..` option.
skip_if: Option<Path>,
/// Contents of the `codec = ..` option.
codec: Option<Path>,
},
/// Maps the field to the compounds' XML element's namespace.
@ -921,6 +924,7 @@ impl XmlFieldMeta {
Vec<Box<XmlFieldMeta>>,
FlagOr<Path>,
Option<Path>,
Option<Path>,
)> {
if meta.input.peek(token::Paren) {
let mut name: Option<NameRef> = None;
@ -928,6 +932,7 @@ impl XmlFieldMeta {
let mut extract: Vec<Box<XmlFieldMeta>> = Vec::new();
let mut skip_if: Option<Path> = None;
let mut default_ = FlagOr::Absent;
let mut codec: Option<Path> = None;
meta.parse_nested_meta(|meta| {
parse_meta!(
meta,
@ -936,18 +941,20 @@ impl XmlFieldMeta {
ParseExtracts(&mut extract),
ParseFlagOr("default", &mut default_),
ParseValue("skip_if", &mut skip_if),
ParseValue("codec", &mut codec),
)
})?;
Ok((namespace, name, extract, default_, skip_if))
Ok((namespace, name, extract, default_, skip_if, codec))
} else {
Ok((None, None, Vec::new(), FlagOr::Absent, None))
Ok((None, None, Vec::new(), FlagOr::Absent, None, None))
}
}
/// Processes a `#[xml(child)]` meta, creating a [`Self::Child`]
/// variant with [`ChildMode::Single`].
fn child_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let (namespace, name, extract, default_, skip_if) = Self::child_common_from_meta(meta)?;
let (namespace, name, extract, default_, skip_if, codec) =
Self::child_common_from_meta(meta)?;
Ok(Self::Child {
mode: ChildMode::Single,
namespace,
@ -955,13 +962,15 @@ impl XmlFieldMeta {
extract,
default_,
skip_if,
codec,
})
}
/// Processes a `#[xml(children)]` meta, creating a [`Self::Child`]
/// variant with [`ChildMode::Collection`].
fn children_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
let (namespace, name, extract, default_, skip_if) = Self::child_common_from_meta(meta)?;
let (namespace, name, extract, default_, skip_if, codec) =
Self::child_common_from_meta(meta)?;
if let Some(default_) = default_.into_span() {
return Err(syn::Error::new(default_, "default cannot be used on #[xml(children)] (it is implied, the default is the empty container)"));
}
@ -972,6 +981,7 @@ impl XmlFieldMeta {
extract,
default_: FlagOr::Absent,
skip_if,
codec,
})
}

View File

@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use crate::core::{
error::{DynNamespaceError, Error},
Base64, DynNamespace, DynNamespaceEnum, FromXml, IntoXml,
Base64, DynNamespace, DynNamespaceEnum, ElementCodec, FromXml, IntoXml,
};
use crate::Element;
@ -1121,3 +1121,57 @@ fn ignore_unknown_attributes_rejects_children() {
other => panic!("unexpected result: {:?}", other),
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS1, name = "single")]
pub struct SingleEl {
#[xml(text)]
pub value: String,
}
impl ElementCodec<String> for SingleEl {
fn decode(value: Self) -> String {
value.value
}
fn encode(value: String) -> Self {
Self { value }
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS1, name = "multi")]
pub struct MultiEl {
#[xml(text)]
pub value: String,
}
impl ElementCodec<String> for MultiEl {
fn decode(value: Self) -> String {
value.value
}
fn encode(value: String) -> Self {
Self { value }
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS1, name = "element-codec")]
pub struct ElementCodecTest {
#[xml(child(codec = SingleEl))]
pub single: String,
#[xml(children(codec = MultiEl))]
pub multi: Vec<String>,
}
#[test]
fn element_codec_roundtrip() {
match crate::util::test::parse_str::<ElementCodecTest>(
"<element-codec xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><single>text 1</single><multi>text 2</multi><multi>text 3</multi></element-codec>",
) {
Ok(_) => (),
other => panic!("unexpected result: {:?}", other),
}
}