xso-proc: provide FromXml::xml_name_matcher on derived impls

That way, derived implementations can also make use of the performance
benefits.
This commit is contained in:
Jonas Schäfer 2025-05-01 16:26:32 +02:00 committed by Link Mauve
commit 59af49c691
5 changed files with 166 additions and 3 deletions

View file

@ -94,6 +94,14 @@ fn empty_roundtrip() {
roundtrip_full::<Empty>("<foo xmlns='urn:example:ns1'/>");
}
#[test]
fn empty_xml_name_matcher_is_specific() {
assert_eq!(
Empty::xml_name_matcher(),
xso::fromxml::XmlNameMatcher::Specific(NS1, "foo")
);
}
#[test]
fn empty_name_mismatch() {
#[allow(unused_imports)]
@ -717,6 +725,14 @@ enum NameSwitchedEnum {
},
}
#[test]
fn name_switched_enum_matcher_is_in_namespace() {
assert_eq!(
NameSwitchedEnum::xml_name_matcher(),
xso::fromxml::XmlNameMatcher::InNamespace(NS1)
);
}
#[test]
fn name_switched_enum_positive_variant_1() {
#[allow(unused_imports)]
@ -1844,6 +1860,14 @@ enum DynamicEnum {
},
}
#[test]
fn dynamic_enum_matcher_is_any() {
assert_eq!(
DynamicEnum::xml_name_matcher(),
xso::fromxml::XmlNameMatcher::Any
);
}
#[test]
fn dynamic_enum_roundtrip_a() {
#[allow(unused_imports)]
@ -1903,6 +1927,27 @@ fn fallible_parse_positive_err() {
}
}
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
#[xml()]
enum DynamicEnumWithSharedNamespace {
#[xml(transparent)]
A(RequiredAttribute),
#[xml(namespace = NS1, name = "b")]
B {
#[xml(text)]
contents: String,
},
}
#[test]
fn dynamic_enum_with_shared_namespace_matcher_is_in_namespace() {
assert_eq!(
DynamicEnumWithSharedNamespace::xml_name_matcher(),
xso::fromxml::XmlNameMatcher::InNamespace(NS1)
);
}
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
#[xml(namespace = NS1, name = "parent")]
struct ExtractTupleToCollection {

View file

@ -7,8 +7,45 @@
//! Definitions common to both enums and structs
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::*;
/// Template which renders to a `xso::fromxml::XmlNameMatcher` value.
pub(crate) enum XmlNameMatcher {
/// Renders as `xso::fromxml::XmlNameMatcher::Any`.
#[allow(dead_code)] // We keep it for completeness.
Any,
/// Renders as `xso::fromxml::XmlNameMatcher::InNamespace(#0)`.
InNamespace(TokenStream),
/// Renders as `xso::fromxml::XmlNameMatcher::Specific(#0, #1)`.
Specific(TokenStream, TokenStream),
/// Renders as `#0`.
///
/// This is an escape hatch for more complicated constructs, e.g. when
/// a superset of multiple matchers is required.
Custom(TokenStream),
}
impl ToTokens for XmlNameMatcher {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Any => tokens.extend(quote! {
::xso::fromxml::XmlNameMatcher::<'static>::Any
}),
Self::InNamespace(ref namespace) => tokens.extend(quote! {
::xso::fromxml::XmlNameMatcher::<'static>::InNamespace(#namespace)
}),
Self::Specific(ref namespace, ref name) => tokens.extend(quote! {
::xso::fromxml::XmlNameMatcher::<'static>::Specific(#namespace, #name)
}),
Self::Custom(ref stream) => tokens.extend(stream.clone()),
}
}
}
/// Parts necessary to construct a `::xso::FromXml` implementation.
pub(crate) struct FromXmlParts {
/// Additional items necessary for the implementation.
@ -19,6 +56,9 @@ pub(crate) struct FromXmlParts {
/// The name of the type which is the `::xso::FromXml::Builder`.
pub(crate) builder_ty_ident: Ident,
/// The `XmlNameMatcher` to pre-select elements for this implementation.
pub(crate) name_matcher: XmlNameMatcher,
}
/// Parts necessary to construct a `::xso::AsXml` implementation.

View file

@ -12,7 +12,7 @@ use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::*;
use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
use crate::common::{AsXmlParts, FromXmlParts, ItemDef, XmlNameMatcher};
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
@ -203,6 +203,16 @@ impl NameSwitchedEnum {
})
}
/// Provide the `XmlNameMatcher` template for this enum.
///
/// Name-switched enums always return a matcher in the namespace of the
/// elements they match against.
fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
Ok(XmlNameMatcher::InNamespace(
self.namespace.to_token_stream(),
))
}
/// Build the deserialisation statemachine for the name-switched enum.
fn make_from_events_statemachine(
&self,
@ -461,6 +471,17 @@ impl AttributeSwitchedEnum {
})
}
/// Provide the `XmlNameMatcher` template for this enum.
///
/// Attribute-switched enums always return a matcher specific to the
/// element they operate on.
fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
Ok(XmlNameMatcher::Specific(
self.elem_namespace.to_token_stream(),
self.elem_name.to_token_stream(),
))
}
/// Build the deserialisation statemachine for the attribute-switched enum.
fn make_from_events_statemachine(
&self,
@ -613,6 +634,31 @@ impl DynamicEnum {
Ok(Self { variants })
}
/// Provide the `XmlNameMatcher` template for this dynamic enum.
///
/// Dynamic enums return the superset of the matchers of their variants,
/// which in many cases will be XmlNameMatcher::Any.
fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
let mut iter = self.variants.iter();
let Some(first) = iter.next() else {
// Since the enum has no variants, it cannot match anything. We
// use an empty namespace and an empty name, which will never
// match.
return Ok(XmlNameMatcher::Custom(quote! {
::xso::fromxml::XmlNameMatcher::<'static>::Specific("", "")
}));
};
let first_matcher = first.inner.xml_name_matcher()?;
let mut composition = quote! { #first_matcher };
for variant in iter {
let next_matcher = variant.inner.xml_name_matcher()?;
composition.extend(quote! { .superset(#next_matcher) });
}
Ok(XmlNameMatcher::Custom(composition))
}
/// Build the deserialisation statemachine for the dynamic enum.
fn make_from_events_statemachine(
&self,
@ -781,6 +827,15 @@ impl EnumInner {
}
}
/// Provide the `XmlNameMatcher` template for this enum.
fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
match self {
Self::NameSwitched(ref inner) => inner.xml_name_matcher(),
Self::AttributeSwitched(ref inner) => inner.xml_name_matcher(),
Self::Dynamic(ref inner) => inner.xml_name_matcher(),
}
}
/// Build the deserialisation statemachine for the enum.
fn make_from_events_statemachine(
&self,
@ -911,6 +966,7 @@ impl ItemDef for EnumDef {
#builder_ty_ident::new(#name_ident, #attrs_ident, ctx)
},
builder_ty_ident: builder_ty_ident.clone(),
name_matcher: self.inner.xml_name_matcher()?,
})
}

View file

@ -71,6 +71,7 @@ fn from_xml_impl(input: Item) -> Result<TokenStream> {
defs,
from_events_body,
builder_ty_ident,
name_matcher,
} = def.make_from_events_builder(&vis, &name_ident, &attrs_ident)?;
#[cfg_attr(not(feature = "minidom"), allow(unused_mut))]
@ -87,6 +88,10 @@ fn from_xml_impl(input: Item) -> Result<TokenStream> {
) -> ::core::result::Result<Self::Builder, ::xso::error::FromEventsError> {
#from_events_body
}
fn xml_name_matcher() -> ::xso::fromxml::XmlNameMatcher<'static> {
#name_matcher
}
}
};

View file

@ -7,10 +7,10 @@
//! Handling of structs
use proc_macro2::{Span, TokenStream};
use quote::quote;
use quote::{quote, ToTokens};
use syn::{spanned::Spanned, *};
use crate::common::{AsXmlParts, FromXmlParts, ItemDef};
use crate::common::{AsXmlParts, FromXmlParts, ItemDef, XmlNameMatcher};
use crate::compound::Compound;
use crate::error_message::ParentRef;
use crate::meta::{reject_key, Flag, NameRef, NamespaceRef, QNameRef, XmlCompoundMeta};
@ -168,6 +168,22 @@ impl StructInner {
}
}
pub(crate) fn xml_name_matcher(&self) -> Result<XmlNameMatcher> {
match self {
Self::Transparent { ty, .. } => Ok(XmlNameMatcher::Custom(quote! {
<#ty as ::xso::FromXml>::xml_name_matcher()
})),
Self::Compound {
xml_namespace,
xml_name,
..
} => Ok(XmlNameMatcher::Specific(
xml_namespace.to_token_stream(),
xml_name.to_token_stream(),
)),
}
}
pub(crate) fn make_from_events_statemachine(
&self,
state_ty_ident: &Ident,
@ -399,6 +415,7 @@ impl ItemDef for StructDef {
#builder_ty_ident::new(#name_ident, #attrs_ident, ctx)
},
builder_ty_ident: builder_ty_ident.clone(),
name_matcher: self.inner.xml_name_matcher()?,
})
}