1
0
mirror of https://gitlab.com/xmpp-rs/xmpp-rs.git synced 2024-06-17 21:35:25 +02:00

parsers-macros: add support for skip_if on child/children fields

This commit is contained in:
Jonas Schäfer 2024-03-30 16:33:44 +01:00
parent 3983e0d705
commit 953de151c8
5 changed files with 80 additions and 14 deletions

View File

@ -278,6 +278,12 @@ The following field kinds are available:
- `extract(..)` (required): Specification of data to extract. See below
for options.
- `skip_if`: If set, this must be the path to a callable. That callable is
invoked with a reference to the field's type at serialisation time. If
the callable returns true, the field is omitted from the output
completely.
This should often be combined with `default`.
- `default`: If the child is not present, the field value will be created
using [`Default::default`]. For `extract(..)`, this is even required when
using `Option<..>` as a field type.

View File

@ -239,6 +239,11 @@ pub(crate) struct ChildField {
/// [`std::default::Default`] if no matching child can be found, instead
/// of aborting parsing with an error.
default_on_missing: Flag,
/// If set, it must point to a function. That function will be called with
/// 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>,
}
impl ChildField {
@ -270,6 +275,7 @@ impl ChildField {
name: Option<NameRef>,
extract: Vec<Box<XmlFieldMeta>>,
default_on_missing: Flag,
skip_if: Option<Path>,
field_type: &Type,
) -> Result<Self> {
if extract.len() > 0 {
@ -316,6 +322,7 @@ impl ChildField {
extract,
single_extract_type,
)?),
skip_if,
default_on_missing,
})
} else {
@ -335,6 +342,7 @@ impl ChildField {
mode,
extract: None,
default_on_missing,
skip_if,
})
}
}
@ -518,10 +526,19 @@ impl ChildField {
ty: Type,
container_namespace_expr: &Expr,
) -> Result<TokenStream> {
let temp_ident = Ident::new("__data", Span::call_site());
let skip_map = match self.skip_if {
Some(callable) => quote! {
match #callable(&#temp_ident) {
false => Some(#temp_ident),
true => None,
}
},
None => quote! { Some(#temp_ident) },
};
match self.mode {
ChildMode::Single => match self.extract {
Some(extract) => {
let temp_ident = Ident::new("__data", Span::call_site());
let temp_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
@ -537,16 +554,19 @@ impl ChildField {
&temp_expr,
)?;
Ok(quote! {
match Option::<#inner_ty>::from(#ident) {
match Option::<#inner_ty>::from(#ident).and_then(|#temp_ident| #skip_map) {
Some(#temp_ident) => builder.append(::xmpp_parsers_core::exports::minidom::Node::Element(#assemble)),
None => builder,
}
})
}
None => Ok(quote! {
match <#ty as ::xmpp_parsers_core::IntoXml>::into_tree(#ident) {
Some(el) => builder.append(::xmpp_parsers_core::exports::minidom::Node::Element(el)),
None => builder,
{
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,
}
}
}),
},
@ -558,20 +578,23 @@ impl ChildField {
&Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: Path::from(Ident::new("data", Span::call_site())),
path: temp_ident.clone().into(),
}),
)?;
Ok(quote! {
builder.append_all(
#ident.into_iter().map(|data| {
#assemble
#ident.into_iter().filter_map(|#temp_ident| {
match #skip_map {
Some(#temp_ident) => Some(#assemble),
None => None,
}
})
)
})
}
None => Ok(quote! {
builder.append_all(#ident.into_iter().filter_map(|item| {
::xmpp_parsers_core::IntoXml::into_tree(item).map(|el| ::xmpp_parsers_core::exports::minidom::Node::Element(el))
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))
}))
}),
},

View File

@ -195,6 +195,7 @@ impl FieldKind {
name,
extract,
default_on_missing,
skip_if,
} => Ok(FieldKind::Child(ChildField::new(
span,
mode,
@ -202,6 +203,7 @@ impl FieldKind {
name,
extract,
default_on_missing,
skip_if,
field_ty,
)?)),
XmlFieldMeta::Text { codec, ty } => {

View File

@ -705,6 +705,9 @@ pub(crate) enum XmlFieldMeta {
/// Presence of the `default` flag.
default_on_missing: Flag,
/// Contents of the `skip_if = ..` option.
skip_if: Option<Path>,
},
/// Maps the field to the compounds' XML element's namespace.
@ -801,11 +804,13 @@ impl XmlFieldMeta {
Option<NameRef>,
Vec<Box<XmlFieldMeta>>,
Flag,
Option<Path>,
)> {
if meta.input.peek(token::Paren) {
let mut name: Option<NameRef> = None;
let mut namespace: Option<NamespaceRef> = None;
let mut extract: Vec<Box<XmlFieldMeta>> = Vec::new();
let mut skip_if: Option<Path> = None;
let mut default_on_missing = Flag::Absent;
meta.parse_nested_meta(|meta| {
parse_meta!(
@ -814,31 +819,35 @@ impl XmlFieldMeta {
ParseValue("namespace", &mut namespace),
ParseExtracts(&mut extract),
ParseFlag("default", &mut default_on_missing),
ParseValue("skip_if", &mut skip_if),
)
})?;
Ok((namespace, name, extract, default_on_missing))
Ok((namespace, name, extract, default_on_missing, skip_if))
} else {
Ok((None, None, Vec::new(), Flag::Absent))
Ok((None, None, Vec::new(), Flag::Absent, 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_on_missing) = Self::child_common_from_meta(meta)?;
let (namespace, name, extract, default_on_missing, skip_if) =
Self::child_common_from_meta(meta)?;
Ok(Self::Child {
mode: ChildMode::Single,
namespace,
name,
extract,
default_on_missing,
skip_if,
})
}
/// 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_on_missing) = Self::child_common_from_meta(meta)?;
let (namespace, name, extract, default_on_missing, skip_if) =
Self::child_common_from_meta(meta)?;
if let Flag::Present(default_on_missing) = default_on_missing {
return Err(syn::Error::new(default_on_missing, "default cannot be used on #[xml(children)] (it is implied, the default is the empty container)"));
}
@ -848,6 +857,7 @@ impl XmlFieldMeta {
name,
extract,
default_on_missing: Flag::Absent,
skip_if,
})
}

View File

@ -975,3 +975,28 @@ fn element_qualified_negative_name() {
other => panic!("unexpected result: {:?}", other),
}
}
fn is_zero(v: &i8) -> bool {
*v == 0
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS1, name = "skip-if")]
pub struct ChildExtractSkipIf {
#[xml(child(namespace = self::TEST_NS1, name = "foo", extract(text), default, skip_if = is_zero))]
pub value: i8,
}
#[test]
fn child_extract_skip_if_roundtrip_absent() {
crate::util::test::roundtrip_full::<ChildExtractSkipIf>(
"<skip-if xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'/>",
);
}
#[test]
fn child_extract_skip_if_roundtrip_present() {
crate::util::test::roundtrip_full::<ChildExtractSkipIf>(
"<skip-if xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2'><foo>10</foo></skip-if>",
);
}