xso: add support for selectively discarding text and attributes

This commit is contained in:
Jonas Schäfer 2025-04-05 13:06:18 +02:00
commit b05ee0280a
8 changed files with 309 additions and 5 deletions

View file

@ -7,12 +7,12 @@
//! Handling of the insides of compound structures (structs and enum variants)
use proc_macro2::{Span, TokenStream};
use quote::quote;
use quote::{quote, ToTokens};
use syn::{spanned::Spanned, *};
use crate::error_message::ParentRef;
use crate::field::{FieldBuilderPart, FieldDef, FieldIteratorPart, FieldTempInit, NestedMatcher};
use crate::meta::NamespaceRef;
use crate::meta::{DiscardSpec, Flag, NameRef, NamespaceRef, QNameRef};
use crate::scope::{mangle_member, AsItemsScope, FromEventsScope};
use crate::state::{AsItemsSubmachine, FromEventsSubmachine, State};
use crate::types::{
@ -55,6 +55,12 @@ pub(crate) struct Compound {
/// Policy defining how to handle unknown children.
unknown_child_policy: Expr,
/// Attributes to discard.
discard_attr: Vec<(Option<NamespaceRef>, NameRef)>,
/// Text to discard.
discard_text: Flag,
}
impl Compound {
@ -63,6 +69,7 @@ impl Compound {
compound_fields: I,
unknown_attribute_policy: Option<Ident>,
unknown_child_policy: Option<Ident>,
discard: Vec<DiscardSpec>,
) -> Result<Self> {
let unknown_attribute_policy = resolve_policy(
unknown_attribute_policy,
@ -96,10 +103,60 @@ impl Compound {
fields.push(field);
}
let mut discard_text = Flag::Absent;
let mut discard_attr = Vec::new();
for spec in discard {
match spec {
DiscardSpec::Text { span } => {
if let Some(field) = text_field.as_ref() {
let mut err = Error::new(
*field,
"cannot combine `#[xml(text)]` field with `discard(text)`",
);
err.combine(Error::new(
spec.span(),
"the discard(text) attribute is here",
));
return Err(err);
}
if let Flag::Present(other) = discard_text {
let mut err = Error::new(
span,
"only one `discard(text)` meta is allowed per compound",
);
err.combine(Error::new(other, "the discard(text) meta is here"));
return Err(err);
}
discard_text = Flag::Present(span);
}
DiscardSpec::Attribute {
qname: QNameRef { namespace, name },
span,
} => {
let xml_namespace = namespace;
let xml_name = match name {
Some(v) => v,
None => {
return Err(Error::new(
span,
"discard(attribute) must specify a name, e.g. via discard(attribute = \"some-name\")",
));
}
};
discard_attr.push((xml_namespace, xml_name));
}
}
}
Ok(Self {
fields,
unknown_attribute_policy,
unknown_child_policy,
discard_attr,
discard_text,
})
}
@ -109,6 +166,7 @@ impl Compound {
container_namespace: &NamespaceRef,
unknown_attribute_policy: Option<Ident>,
unknown_child_policy: Option<Ident>,
discard: Vec<DiscardSpec>,
) -> Result<Self> {
Self::from_field_defs(
compound_fields.iter().enumerate().map(|(i, field)| {
@ -127,6 +185,7 @@ impl Compound {
}),
unknown_attribute_policy,
unknown_child_policy,
discard,
)
}
@ -165,7 +224,15 @@ impl Compound {
let mut output_cons = TokenStream::default();
let mut child_matchers = TokenStream::default();
let mut fallback_child_matcher = None;
let mut text_handler = None;
let mut text_handler = if self.discard_text.is_set() {
Some(quote! {
::core::result::Result::Ok(::core::ops::ControlFlow::Break(
Self::#default_state_ident { #builder_data_ident }
))
})
} else {
None
};
let mut extra_defs = TokenStream::default();
let is_tuple = !output_name.is_path();
@ -329,6 +396,19 @@ impl Compound {
}
}
let mut discard_attr = TokenStream::default();
for (xml_namespace, xml_name) in self.discard_attr.iter() {
let xml_namespace = match xml_namespace {
Some(v) => v.to_token_stream(),
None => quote! {
::xso::exports::rxml::Namespace::none()
},
};
discard_attr.extend(quote! {
let _ = #attrs.remove(#xml_namespace, #xml_name);
});
}
let text_handler = match text_handler {
Some(v) => v,
None => quote! {
@ -442,6 +522,7 @@ impl Compound {
let #builder_data_ident = #builder_data_ty {
#builder_data_init
};
#discard_attr
if #attrs.len() > 0 {
let _: () = #unknown_attribute_policy.apply_policy(#unknown_attr_err)?;
}

View file

@ -49,6 +49,7 @@ impl NameVariant {
on_unknown_attribute,
on_unknown_child,
transparent,
discard,
} = XmlCompoundMeta::parse_from_attributes(&decl.attrs)?;
reject_key!(debug flag not on "enum variants" only on "enums and structs");
@ -70,6 +71,7 @@ impl NameVariant {
enum_namespace,
on_unknown_attribute,
on_unknown_child,
discard,
)?,
})
}
@ -275,6 +277,7 @@ impl DynamicVariant {
on_unknown_attribute: _, // used by StructInner
on_unknown_child: _, // used by StructInner
transparent: _, // used by StructInner
discard: _, // used by StructInner
} = meta;
reject_key!(debug flag not on "enum variants" only on "enums and structs");
@ -391,6 +394,7 @@ impl EnumInner {
on_unknown_attribute,
on_unknown_child,
transparent,
discard,
} = meta;
// These must've been cleared by the caller. Because these being set
@ -404,6 +408,7 @@ impl EnumInner {
reject_key!(transparent flag not on "enums" only on "structs");
reject_key!(on_unknown_attribute not on "enums" only on "enum variants and structs");
reject_key!(on_unknown_child not on "enums" only on "enum variants and structs");
reject_key!(discard vec not on "enums" only on "enum variants and structs");
if let Some(namespace) = namespace {
Ok(Self::NameSwitched(NameSwitchedEnum::new(

View file

@ -391,8 +391,12 @@ fn new_field(
&xml_namespace,
));
}
let parts =
Compound::from_field_defs(field_defs, on_unknown_attribute, on_unknown_child)?;
let parts = Compound::from_field_defs(
field_defs,
on_unknown_attribute,
on_unknown_child,
vec![],
)?;
Ok(Box::new(ChildField {
default_,

View file

@ -60,6 +60,25 @@ macro_rules! reject_key {
));
}
};
($key:ident vec not on $not_allowed_on:literal $(only on $only_allowed_on:literal)?) => {
if let Some(ref $key) = $key.first() {
return Err(Error::new(
$key.span(),
concat!(
"`",
stringify!($key),
"` is not allowed on ",
$not_allowed_on,
$(
" (only on ",
$only_allowed_on,
")",
)?
),
));
}
};
}
pub(crate) use reject_key;
@ -328,6 +347,69 @@ impl QNameRef {
}
}
/// Identifies XML content to discard.
#[derive(Debug)]
pub(crate) enum DiscardSpec {
/// `#[xml(discard(attribute..))]`
Attribute {
/// The span of the nested meta from which this was parsed.
///
/// This is useful for error messages.
span: Span,
/// The value assigned to `namespace` and `name` fields inside
/// `#[xml(discard(attribute(..)))]`, if any.
qname: QNameRef,
},
/// `#[xml(discard(text))]`
Text {
/// The span of the nested meta from which this was parsed.
///
/// This is useful for error messages.
span: Span,
},
}
impl DiscardSpec {
pub(crate) fn span(&self) -> Span {
match self {
Self::Attribute { ref span, .. } => *span,
Self::Text { ref span, .. } => *span,
}
}
}
impl TryFrom<XmlFieldMeta> for DiscardSpec {
type Error = syn::Error;
fn try_from(other: XmlFieldMeta) -> Result<Self> {
match other {
XmlFieldMeta::Attribute {
span,
qname,
default_,
type_,
codec,
} => {
reject_key!(default_ flag not on "discard specifications" only on "fields");
reject_key!(type_ not on "discard specifications" only on "fields");
reject_key!(codec not on "discard specifications" only on "fields");
Ok(Self::Attribute { span, qname })
}
XmlFieldMeta::Text { span, type_, codec } => {
reject_key!(type_ not on "discard specifications" only on "fields");
reject_key!(codec not on "discard specifications" only on "fields");
Ok(Self::Text { span })
}
other => Err(Error::new(
other.span(),
"cannot discard this kind of child",
)),
}
}
}
/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
#[derive(Debug)]
pub(crate) struct XmlCompoundMeta {
@ -362,6 +444,9 @@ pub(crate) struct XmlCompoundMeta {
/// The transparent flag.
pub(crate) transparent: Flag,
/// Items to discard.
pub(crate) discard: Vec<DiscardSpec>,
}
impl XmlCompoundMeta {
@ -378,6 +463,7 @@ impl XmlCompoundMeta {
let mut debug = Flag::Absent;
let mut exhaustive = Flag::Absent;
let mut transparent = Flag::Absent;
let mut discard = Vec::new();
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("debug") {
@ -428,6 +514,12 @@ impl XmlCompoundMeta {
}
transparent = (&meta.path).into();
Ok(())
} else if meta.path.is_ident("discard") {
meta.parse_nested_meta(|meta| {
discard.push(XmlFieldMeta::parse_from_meta(meta)?.try_into()?);
Ok(())
})?;
Ok(())
} else {
match qname.parse_incremental_from_meta(meta)? {
None => Ok(()),
@ -446,6 +538,7 @@ impl XmlCompoundMeta {
on_unknown_child,
exhaustive,
transparent,
discard,
})
}

View file

@ -72,6 +72,7 @@ impl StructInner {
on_unknown_attribute,
on_unknown_child,
transparent,
discard,
} = meta;
// These must've been cleared by the caller. Because these being set
@ -88,6 +89,7 @@ impl StructInner {
reject_key!(name not on "transparent structs");
reject_key!(on_unknown_attribute not on "transparent structs");
reject_key!(on_unknown_child not on "transparent structs");
reject_key!(discard vec not on "transparent structs");
let fields_span = fields.span();
let fields = match fields {
@ -152,6 +154,7 @@ impl StructInner {
&xml_namespace,
on_unknown_attribute,
on_unknown_child,
discard,
)?,
xml_namespace,
xml_name,