1
0
mirror of https://gitlab.com/xmpp-rs/xmpp-rs.git synced 2024-06-09 01:34:03 +02:00

parsers-macros: add support for element-transparent structs

See docs for what they are. They are needed for Jingle, yuck.
This commit is contained in:
Jonas Schäfer 2024-03-29 10:25:51 +01:00
parent 618ef020f9
commit 32121f29a9
4 changed files with 383 additions and 4 deletions

View File

@ -94,7 +94,22 @@ preserved, if the container preserves sort order on insertion.
If set, the parsing is fully delegated to the inner field, which must in
turn implement `FromXml` (or `IntoXml` respectively).
Attributes on the single struct field are ignored.
Attributes on the single struct field are rejected.
`validate` is allowed and will be called.
- `element`, `element(..): Only allowed on tuple-like structs with exactly one
field. That field must be of type [`minidom::Element`].
Supports the following optional inner attributes:
- `namespace`: If given, restricts the namespace of the elements to parse.
Has no influence on XML generation: If the inner element has a different
namespace when the struct is serialized, that namespace will be used.
- `name`: If given, restricts the XML name of the elements to parse.
Has no influence on XML generation: If the inner element has a different
XML name when the struct is serialized, that namespace will be used.
`validate` is allowed and will be called.

View File

@ -122,6 +122,26 @@ impl<'a, T: parse::Parse> MetaParse for ParseValue<'a, T> {
}
}
/// Parse a `NodeFilterMeta` from a meta.
struct ParseNodeFilter<'a>(&'static str, &'a mut Option<NodeFilterMeta>);
impl<'a> MetaParse for ParseNodeFilter<'a> {
fn name(&self) -> &'static str {
self.0
}
fn force_parse_at_meta<'x>(&mut self, meta: meta::ParseNestedMeta<'x>) -> Result<()> {
if self.1.is_some() {
return Err(Error::new_spanned(
meta.path,
format!("duplicate {} option", self.name()),
));
}
*self.1 = Some(NodeFilterMeta::parse_from_meta(meta)?);
Ok(())
}
}
/// Parse a `Vec<ExtractMeta>` from a meta.
struct ParseExtracts<'a>(&'a mut Vec<ExtractMeta>);
@ -314,6 +334,47 @@ impl ToTokens for Name {
}
}
/// Struct containing namespace and name matchers.
#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) struct NodeFilterMeta {
/// The value of the `namespace` option.
pub(crate) namespace: Option<NamespaceRef>,
/// The value of the `name` option.
pub(crate) name: Option<NameRef>,
}
impl NodeFilterMeta {
/// Parse the struct's contents from a potenially nested meta.
///
/// This supports three syntaxes, assuming that `foo` is the meta we're
/// at:
///
/// - `#[xml(.., foo, ..)]` (no value at all): All pieces of the returned
/// struct are `None`.
///
/// - `#[xml(.., foo(..), ..)]`, where `foo` may contain the keys
/// `namespace` and `name`, specifying the values of the struct
/// respectively.
fn parse_from_meta(meta: meta::ParseNestedMeta<'_>) -> Result<Self> {
if meta.input.peek(token::Paren) {
let mut name: Option<NameRef> = None;
let mut namespace: Option<NamespaceRef> = None;
meta.parse_nested_meta(|meta| {
parse_meta!(
meta,
ParseValue("name", &mut name),
ParseValue("namespace", &mut namespace),
)
})?;
Ok(Self { namespace, name })
} else {
Ok(Self::default())
}
}
}
/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
///
/// This is the counterpart to [`XmlFieldMeta`].
@ -356,6 +417,9 @@ pub(crate) struct XmlCompoundMeta {
/// Flag indicating the presence of `debug` inside `#[xml(..)]`
pub(crate) debug: Flag,
/// The options set inside `element`, if any.
pub(crate) element: Option<NodeFilterMeta>,
}
impl XmlCompoundMeta {
@ -375,6 +439,7 @@ impl XmlCompoundMeta {
let mut validate: Option<Path> = None;
let mut prepare: Option<Path> = None;
let mut normalize_with: Option<Path> = None;
let mut element: Option<NodeFilterMeta> = None;
attr.parse_nested_meta(|meta| {
parse_meta!(
@ -391,6 +456,7 @@ impl XmlCompoundMeta {
ParseValue("prepare", &mut prepare),
ParseValue("normalize_with", &mut normalize_with),
ParseValue("normalise_with", &mut normalize_with),
ParseNodeFilter("element", &mut element),
)
})?;
@ -415,6 +481,7 @@ impl XmlCompoundMeta {
prepare,
debug,
normalize_with,
element,
})
}

View File

@ -14,7 +14,9 @@ use syn::*;
use crate::common::{bake_generics, build_prepare, build_validate};
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::meta::{Flag, Name, NameRef, NamespaceRef, StaticNamespace, XmlCompoundMeta};
use crate::meta::{
Flag, Name, NameRef, NamespaceRef, NodeFilterMeta, StaticNamespace, XmlCompoundMeta,
};
/// A XML namespace as declared on a struct.
#[cfg_attr(feature = "debug", derive(Debug))]
@ -42,6 +44,87 @@ pub(crate) enum StructNamespace {
},
}
/// Represent a selector for element-transparent structs.
///
/// See also [`StructInner::Element`].
#[cfg_attr(feature = "debug", derive(Debug))]
pub(crate) enum ElementSelector {
/// Any element will be accepted.
///
/// Corresponds to `#[xml(element)]`.
Any,
/// The element will be matched by XML name only.
///
/// Corresponds to `#[xml(element(name = ..))]`.
ByName(Name),
/// The element will be matched by XML namespace only.
///
/// Corresponds to `#[xml(element(namespace = ..))]`.
ByNamespace(StaticNamespace),
/// The element will be matched by XML namespace and name..
///
/// Corresponds to `#[xml(element(namespace = .., name = ..))]`.
Qualified {
/// The XML namespace to match.
namespace: StaticNamespace,
/// The XML name to match.
name: Name,
},
}
impl TryFrom<NodeFilterMeta> for ElementSelector {
type Error = Error;
fn try_from(other: NodeFilterMeta) -> Result<Self> {
let namespace = match other.namespace {
None => None,
Some(NamespaceRef::Static(ns)) => Some(ns),
Some(NamespaceRef::Dyn(ns)) => return Err(Error::new_spanned(
ns,
"namespace = dyn cannot be used with element-transparent structs or enum variants."
)),
Some(NamespaceRef::Super(ns)) => return Err(Error::new_spanned(
ns,
"namespace = super cannot be used with element-transparent structs or enum variants."
)),
};
let name = other.name.map(|x| Name::from(x));
match (namespace, name) {
(Some(namespace), Some(name)) => Ok(Self::Qualified { namespace, name }),
(Some(namespace), None) => Ok(Self::ByNamespace(namespace)),
(None, Some(name)) => Ok(Self::ByName(name)),
(None, None) => Ok(Self::Any),
}
}
}
impl ElementSelector {
/// Construct a token stream evaluating to bool.
///
/// If the `minidom::Element` in `residual` matches the selector, the
/// token stream will evaluate to true. Otherwise, it will evaluate to
/// false.
fn build_test(self, residual: &Ident) -> TokenStream {
match self {
Self::Any => quote! { true },
Self::ByName(name) => quote! {
#residual.name() == #name
},
Self::ByNamespace(ns) => quote! {
#residual.ns() == #ns
},
Self::Qualified { namespace, name } => quote! {
#residual.is(#name, #namespace)
},
}
}
}
/// The inner parts of the struct.
///
/// This contains all data necessary for the matching logic, but does not
@ -65,6 +148,16 @@ pub(crate) enum StructInner {
ty: Type,
},
/// Single-field tuple-like struct declared with `#[xml(element)]`.
///
/// Element-transparent structs take the incoming XML element as-is, and
/// re-serialise it as-is.
Element {
/// Determines the set of acceptable XML elements. Elements which do
/// not match the selector will not be parsed.
selector: ElementSelector,
},
/// A compound of fields, *not* declared as transparent.
///
/// This can be a unit, tuple-like, or named struct.
@ -97,7 +190,28 @@ impl StructInner {
assert!(meta.attribute.is_none());
assert!(meta.value.is_none());
if let Flag::Present(_) = meta.transparent {
if let Some(element) = meta.element {
if let Flag::Present(transparent) = meta.transparent {
return Err(Error::new(
transparent,
"transparent option conflicts with element option. pick one or the other.",
));
}
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
"namespace option not allowed on element-transparent structs or enum variants",
));
}
if let Some(name) = meta.name {
return Err(Error::new_spanned(
name,
"name option not allowed on element-transparent structs or enum variants",
));
}
Self::new_element(element, fields)
} else if let Flag::Present(_) = meta.transparent {
if let Some(namespace) = meta.namespace {
return Err(Error::new_spanned(
namespace,
@ -179,6 +293,54 @@ impl StructInner {
})
}
/// Construct a new element-transparent struct with the given fields.
///
/// This function ensures that only a single, unnamed field is inside the
/// struct and causes a compile-time error otherwise.
fn new_element(node_filter: NodeFilterMeta, fields: &Fields) -> Result<Self> {
let field = match fields {
Fields::Unit => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
))
}
Fields::Named(_) => {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must be tuple-like",
))
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() == 0 {
return Err(Error::new(
Span::call_site(),
"transparent structs or enum variants must have exactly one field",
));
} else if fields.unnamed.len() > 1 {
return Err(Error::new_spanned(
&fields.unnamed[1],
"transparent structs or enum variants must have exactly one field",
));
}
&fields.unnamed[0]
}
};
for attr in field.attrs.iter() {
if attr.path().is_ident("xml") {
return Err(Error::new_spanned(
attr.path(),
"the field inside a #[xml(transparent)] struct or enum variant cannot have an #[xml(..)] attribute."
));
}
}
Ok(Self::Element {
selector: node_filter.try_into()?,
})
}
/// Construct a new compound-based struct with the given namespace, name
/// and fields.
fn new_compound(namespace: NamespaceRef, name: NameRef, fields: &Fields) -> Result<Self> {
@ -255,6 +417,20 @@ impl StructInner {
}
})
}
Self::Element { selector } => {
let test = selector.build_test(residual);
let cons = match struct_name {
ParentRef::Named(path) => quote! { #path },
ParentRef::Unnamed { .. } => quote! {},
};
Ok(quote! {
if #test {
Ok(#cons ( #residual ))
} else {
Err(#residual)
}
})
}
Self::Compound {
namespace,
name: xml_name,
@ -322,6 +498,15 @@ impl StructInner {
<#ty as ::xmpp_parsers_core::IntoXml>::into_tree(#ident).expect("inner element did not produce any data")
})
}
Self::Element { .. } => {
let ident = access_field(Member::Unnamed(Index {
index: 0,
span: Span::call_site(),
}));
Ok(quote! {
#ident
})
}
Self::Compound {
namespace,
name: xml_name,
@ -381,7 +566,7 @@ impl StructInner {
/// the struct's fields in declaration order.
pub(crate) fn iter_members(&self) -> Box<dyn Iterator<Item = Member> + '_> {
match self {
Self::Transparent { .. } => Box::new(
Self::Transparent { .. } | Self::Element { .. } => Box::new(
[Member::Unnamed(Index {
index: 0,
span: Span::call_site(),

View File

@ -863,3 +863,115 @@ fn attribute_switched_enum_normalized() {
other => panic!("unexpected result: {:?}", other),
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(element(namespace = self::TEST_NS1))]
pub struct ElementByNamespace(Element);
#[test]
fn element_by_namespace_roundtrip_1() {
crate::util::test::roundtrip_full::<ElementByNamespace>(
"<quak xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></quak>",
);
}
#[test]
fn element_by_namespace_roundtrip_2() {
crate::util::test::roundtrip_full::<ElementByNamespace>(
"<fnord xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></fnord>",
);
}
#[test]
fn element_by_namespace_negative() {
match crate::util::test::parse_str::<ElementByNamespace>(
"<quak xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></quak>",
) {
Err(Error::TypeMismatch(_, _, _)) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(element(name = "quak"))]
pub struct ElementByName(Element);
#[test]
fn element_by_name_roundtrip_1() {
crate::util::test::roundtrip_full::<ElementByName>(
"<quak xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></quak>",
);
}
#[test]
fn element_by_name_roundtrip_2() {
crate::util::test::roundtrip_full::<ElementByName>(
"<quak xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></quak>",
);
}
#[test]
fn element_by_name_negative() {
match crate::util::test::parse_str::<ElementByName>(
"<no-quak xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></no-quak>",
) {
Err(Error::TypeMismatch(_, _, _)) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(element)]
pub struct ElementTransparent(Element);
#[test]
fn element_transparent_roundtrip_1() {
crate::util::test::roundtrip_full::<ElementTransparent>(
"<quak xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></quak>",
);
}
#[test]
fn element_transparent_roundtrip_2() {
crate::util::test::roundtrip_full::<ElementTransparent>(
"<quak xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></quak>",
);
}
#[test]
fn element_transparent_roundtrip_3() {
crate::util::test::roundtrip_full::<ElementTransparent>(
"<fnord xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></fnord>",
);
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(element(namespace = self::TEST_NS1, name = "quak"))]
pub struct ElementQualified(Element);
#[test]
fn element_qualified_roundtrip() {
crate::util::test::roundtrip_full::<ElementQualified>(
"<quak xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></quak>",
);
}
#[test]
fn element_qualified_negative_namespace() {
match crate::util::test::parse_str::<ElementQualified>(
"<quak xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></quak>",
) {
Err(Error::TypeMismatch(_, _, _)) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn element_qualified_negative_name() {
match crate::util::test::parse_str::<ElementQualified>(
"<no-quak xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' a1='v1' a2='v2'><foo/><bar xmlns='quak'/></no-quak>",
) {
Err(Error::TypeMismatch(_, _, _)) => (),
other => panic!("unexpected result: {:?}", other),
}
}