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:
parent
618ef020f9
commit
32121f29a9
|
@ -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.
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user