xso_proc: add support for switching on an attribute key/value pair

Needed for the `http_upload::Header` enum, but could also be interesting
to use on an IQ stanza in the future.
This commit is contained in:
Jonas Schäfer 2024-03-28 17:06:20 +01:00
parent 517339e375
commit cac4e08484
7 changed files with 543 additions and 53 deletions

View File

@ -729,3 +729,111 @@ fn extract_field_with_super_negative_wrong_name() {
other => panic!("unexpected result: {:?}", other),
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS1, name = "attr-switched-enum", attribute = "key", exhaustive)]
pub enum AttributeSwitchedEnum {
#[xml(value = "variant-1")]
Variant1 {
#[xml(attribute(name = "variant-1-attr"))]
data: String,
},
#[xml(value = "variant-2")]
Variant2 {
#[xml(attribute(name = "variant-2-attr"))]
data: String,
},
}
#[test]
fn attribute_switched_enum_roundtrip_variant_1() {
crate::util::test::roundtrip_full::<AttributeSwitchedEnum>(
"<attr-switched-enum xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' key='variant-1' variant-1-attr='data'/>",
);
}
#[test]
fn attribute_switched_enum_roundtrip_variant_2() {
crate::util::test::roundtrip_full::<AttributeSwitchedEnum>(
"<attr-switched-enum xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' key='variant-2' variant-2-attr='data'/>",
);
}
#[test]
fn attribute_switched_enum_matches_namespace() {
match crate::util::test::parse_str::<AttributeSwitchedEnum>(
"<attr-switched-enum xmlns='urn:uuid:9a1f4eab-1cfd-464c-a16a-282877cd516f' key='variant-2' variant-2-attr='data'/>",
) {
Err(Error::TypeMismatch(_, _, _)) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn attribute_switched_enum_matches_name() {
match crate::util::test::parse_str::<AttributeSwitchedEnum>(
"<other-switched-enum xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' key='variant-2' variant-2-attr='data'/>",
) {
Err(Error::TypeMismatch(_, _, _)) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn attribute_switched_enum_rejects_unknown_value() {
match crate::util::test::parse_str::<AttributeSwitchedEnum>(
"<attr-switched-enum xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' key='quak'/>",
) {
Err(Error::ParseError(msg)) if msg.find("This is not a").is_some() => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn attribute_switched_enum_rejects_missing_attribute() {
match crate::util::test::parse_str::<AttributeSwitchedEnum>(
"<attr-switched-enum xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' fnord='variant-2'/>",
) {
Err(Error::ParseError(msg)) if msg.find("discriminator attribute").is_some() => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[derive(FromXml, IntoXml, PartialEq, Clone, Debug)]
#[xml(namespace = self::TEST_NS1, name = "attr-switched-enum-fallback", attribute = "key")]
pub enum AttributeSwitchedEnumFallback {
#[xml(value = "variant-1")]
Variant1 {
#[xml(attribute(name = "variant-1-attr"))]
data: String,
},
#[xml(value = "variant-2", fallback)]
Variant2 {
#[xml(attribute(name = "variant-2-attr"))]
data: String,
},
}
#[test]
fn attribute_switched_enum_fallback_roundtrip_variant_1() {
crate::util::test::roundtrip_full::<AttributeSwitchedEnumFallback>(
"<attr-switched-enum-fallback xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' key='variant-1' variant-1-attr='data'/>",
);
}
#[test]
fn attribute_switched_enum_fallback_roundtrip_variant_2() {
crate::util::test::roundtrip_full::<AttributeSwitchedEnumFallback>(
"<attr-switched-enum-fallback xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' key='variant-2' variant-2-attr='data'/>",
);
}
#[test]
fn attribute_switched_enum_fallback() {
match crate::util::test::parse_str::<AttributeSwitchedEnumFallback>(
"<attr-switched-enum-fallback xmlns='urn:uuid:41854041-fa04-4e2b-94ae-ffaefb6b24e2' key='variant-3' variant-2-attr='data'/>",
) {
Ok(v) => assert_eq!(v, AttributeSwitchedEnumFallback::Variant2 { data: "data".to_string() }),
other => panic!("unexpected result: {:?}", other),
}
}

View File

@ -118,17 +118,28 @@ impl Compound {
/// `std::cmp::PartialEq<str>` context.
///
/// - `residual` must be the identifier at which the element is found.
/// - `forgive_attributes` must be a (potentially empty) slice of XML
/// attribute names to ignore during the unknown attribute check. This
/// can be used to ignore attributes which have been used in element
/// matching (e.g. enum discriminators).
pub(crate) fn build_try_from_element(
self,
container_name: &ParentRef,
container_namespace_expr: &Expr,
residual: &Ident,
forgive_attributes: &[&str],
) -> Result<TokenStream> {
let readable_name = container_name.to_string();
let mut init = quote! {};
let mut tupinit = quote! {};
let mut attrcheck = quote! {};
let mut attrcheck = quote! {
#(
if key == #forgive_attributes {
continue;
}
)*
};
let mut tempinit = quote! {};
let mut childiter = quote! {};
let mut childfallback = quote! {

View File

@ -52,11 +52,15 @@ fn build_ident_mapping<I: Iterator<Item = Member>>(
(orig_names, mapped_names, Box::new(map_ident))
}
/// Variant of an enum which switches on the XML element's name.
/// Variant of an enum which switches on a string value extracted from the XML
/// element.
///
/// The caller of the respective methods decides what string the variant
/// matches against and how that match is carried out.
#[cfg_attr(feature = "debug", derive(Debug))]
struct XmlNameVariant {
/// The XML name to match against.
xml_name: Name,
struct StrMatchedVariant {
/// The string to match against.
value: LitStr,
/// The identifier of the enum variant.
ident: Ident,
@ -68,18 +72,69 @@ struct XmlNameVariant {
inner: Compound,
}
impl XmlNameVariant {
/// Parse a [`syn::Variant`] as XML name switched variant.
fn new(variant: &Variant) -> Result<Self> {
let meta = XmlCompoundMeta::parse_from_attributes(&variant.attrs)?;
if let Some(namespace) = meta.namespace {
impl StrMatchedVariant {
fn prevalidate(meta: &mut XmlCompoundMeta) -> Result<()> {
if let Some(namespace) = meta.namespace.take() {
return Err(Error::new_spanned(
namespace,
"`namespace` not allowed on enum variants.",
));
}
if let Some(attribute) = meta.attribute.take() {
return Err(Error::new_spanned(
attribute,
"`attribute` not allowed on enum variants.",
));
}
if let Flag::Present(transparent) = meta.transparent.take() {
return Err(Error::new(
transparent,
"`transparent` not allowed on enum variants in enums with a fixed namespace.",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(Error::new(
exhaustive,
"`exhaustive` not allowed on enum variants.",
));
}
if let Some(validate) = meta.validate.take() {
return Err(Error::new_spanned(
validate,
"`validate` not allowed on enum variants.",
));
}
if let Some(prepare) = meta.prepare.take() {
return Err(Error::new_spanned(
prepare,
"`validate` not allowed on enum variants.",
));
}
if let Flag::Present(debug) = meta.debug.take() {
return Err(Error::new(debug, "`debug` not allowed on enum variants."));
}
Ok(())
}
/// Parse a [`syn::Variant`] as XML name switched variant.
fn new_xml_name_switched(variant: &Variant) -> Result<Self> {
let mut meta = XmlCompoundMeta::parse_from_attributes(&variant.attrs)?;
Self::prevalidate(&mut meta)?;
if let Some(value) = meta.value {
return Err(Error::new_spanned(
value,
"`value` not allowed on enum variants inside enums matching on XML name.",
));
}
let name = if let Some(name) = meta.name {
name.into()
} else {
@ -91,40 +146,37 @@ impl XmlNameVariant {
let fallback = meta.fallback;
if let Flag::Present(transparent) = meta.transparent {
return Err(Error::new(
transparent,
"`transparent` not allowed on enum variants in enums with a fixed namespace.",
));
}
Ok(Self {
value: name,
ident: variant.ident.clone(),
fallback,
inner: Compound::from_fields(&variant.fields)?,
})
}
if let Flag::Present(exhaustive) = meta.exhaustive {
return Err(Error::new(
exhaustive,
"`exhaustive` not allowed on enum variants.",
));
}
/// Parse a [`syn::Variant`] as XML attribute value switched variant.
fn new_xml_attribute_switched(variant: &Variant) -> Result<Self> {
let mut meta = XmlCompoundMeta::parse_from_attributes(&variant.attrs)?;
Self::prevalidate(&mut meta)?;
if let Some(validate) = meta.validate {
if let Some(name) = meta.name {
return Err(Error::new_spanned(
validate,
"`validate` not allowed on enum variants.",
name,
"`name` not allowed on enum variants inside enums matching on attribute values.",
));
}
if let Some(prepare) = meta.prepare {
return Err(Error::new_spanned(
prepare,
"`validate` not allowed on enum variants.",
let Some(value) = meta.value else {
return Err(Error::new(
meta.span,
"`value` is required on enum variants in enums matching on attribute values.",
));
}
};
if let Flag::Present(debug) = meta.debug {
return Err(Error::new(debug, "`debug` not allowed on enum variants."));
}
let fallback = meta.fallback;
Ok(Self {
xml_name: name,
value,
ident: variant.ident.clone(),
fallback,
inner: Compound::from_fields(&variant.fields)?,
@ -207,7 +259,7 @@ struct XmlNameSwitched {
namespace: StaticNamespace,
/// The enum's variants.
variants: Vec<XmlNameVariant>,
variants: Vec<StrMatchedVariant>,
}
impl XmlNameSwitched {
@ -228,7 +280,7 @@ impl XmlNameSwitched {
let mut variants = Vec::with_capacity(input.size_hint().1.unwrap_or(0));
let mut had_fallback = false;
for variant in input {
let variant = XmlNameVariant::new(variant)?;
let variant = StrMatchedVariant::new_xml_name_switched(variant)?;
if let Flag::Present(fallback) = variant.fallback {
if had_fallback {
return Err(syn::Error::new(
@ -282,7 +334,7 @@ impl XmlNameSwitched {
for variant in self.variants {
let ident = variant.ident;
let xml_name = variant.xml_name;
let xml_name = variant.value;
let variant_impl = variant.inner.build_try_from_element(
&(Path {
@ -297,6 +349,7 @@ impl XmlNameSwitched {
.into()),
&namespace_expr,
residual,
&[],
)?;
let variant_impl = quote! {
@ -351,7 +404,7 @@ impl XmlNameSwitched {
let builder = Ident::new("builder", Span::call_site());
let mut matchers = quote! {};
for variant in self.variants {
let xml_name = variant.xml_name;
let xml_name = variant.value;
let path = Path {
leading_colon: None,
segments: [
@ -388,6 +441,218 @@ impl XmlNameSwitched {
}
}
/// An enum which switches on the value of an attribute of the XML element.
#[cfg_attr(feature = "debug", derive(Debug))]
struct XmlAttributeSwitched {
/// The namespace the enum's XML element resides in.
namespace: StaticNamespace,
/// The XML name of the element.
name: Name,
/// The name of the XML attribute to read.
attribute_name: LitStr,
/// The enum's variants.
variants: Vec<StrMatchedVariant>,
}
impl XmlAttributeSwitched {
/// Construct a new XML name switched enum from parts.
///
/// - `exhaustive` must be the exhaustive flag. Currently, this must be
/// set on [`XmlAttributeSwitched`] enums.
///
/// - `namespace` must be the XML namespace of the enum.
///
/// - `input` must be an iterator emitting borrowed [`syn::Variant`]
/// structs to process.
fn new<'x, I: Iterator<Item = &'x Variant>>(
exhaustive: Flag,
namespace: StaticNamespace,
name: Name,
attribute_name: LitStr,
input: I,
) -> Result<Self> {
let mut variants = Vec::with_capacity(input.size_hint().1.unwrap_or(0));
let mut had_fallback = false;
for variant in input {
let variant = StrMatchedVariant::new_xml_attribute_switched(variant)?;
if let Flag::Present(fallback) = variant.fallback {
if had_fallback {
return Err(syn::Error::new(
fallback,
"only one variant may be a fallback variant",
));
}
had_fallback = true;
}
variants.push(variant);
}
if let Flag::Present(exhaustive) = exhaustive {
if had_fallback {
return Err(syn::Error::new(exhaustive, "exhaustive cannot be sensibly combined with a fallback variant. choose one or the other."));
}
} else {
if !had_fallback {
return Err(syn::Error::new_spanned(
attribute_name,
"enums switching on an attribute must be marked exhaustive or have a fallback variant."
));
}
}
Ok(Self {
namespace,
name,
attribute_name,
variants,
})
}
/// Construct an expression which consumes the `minidom::Element` at
/// `residual` and returns a `Result<T, Error>`.
///
/// - `enum_ident` must be the identifier of the enum's type.
/// - `validate` must be a statement which takes a mutable reference to
/// the identifier `result` and `return`'s with an error if it is not
/// acceptable.
/// - `residual` must be the identifier at which the element is found.
fn build_try_from_element(
self,
enum_ident: &Ident,
validate: Stmt,
residual: &Ident,
) -> Result<TokenStream> {
let xml_namespace = self.namespace;
let xml_name = self.name;
let attribute_name = self.attribute_name.value();
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: xml_namespace.clone(),
});
let mut fallback: Option<TokenStream> = None;
let mut iter = quote! {};
for variant in self.variants {
let ident = variant.ident;
let xml_name = variant.value;
let variant_impl = variant.inner.build_try_from_element(
&(Path {
leading_colon: None,
segments: [
PathSegment::from(enum_ident.clone()),
PathSegment::from(ident.clone()),
]
.into_iter()
.collect(),
}
.into()),
&namespace_expr,
residual,
&[attribute_name.as_str()],
)?;
let variant_impl = quote! {
let mut result = #variant_impl;
#validate
Ok(result)
};
if variant.fallback.is_set() {
fallback = Some(quote! {
_ => { #variant_impl },
})
}
iter = quote! {
#iter
#xml_name => { #variant_impl },
};
}
let fallback = fallback.unwrap_or_else(|| quote! {
_ => Err(::xso::error::Error::ParseError(concat!("This is not a ", stringify!(#enum_ident), " element."))),
});
let on_missing = format!(
"Required discriminator attribute '{}' on enum {} element missing.",
attribute_name, enum_ident,
);
Ok(quote! {
if #residual.is(#xml_name, #xml_namespace) {
match #residual.attr(#attribute_name) {
Some(v) => match v {
#iter
#fallback
}
None => Err(::xso::error::Error::ParseError(#on_missing)),
}
} else {
Err(::xso::error::Error::TypeMismatch("", "", #residual))
}
})
}
/// Construct a token stream which contains the arms of a `match`
/// expression dissecting the enum and building `minidom::Element` objects
/// for each variant.
///
/// `ty_ident` must be the identifier of the enum's type.
fn build_into_element(self, ty_ident: &Ident) -> Result<TokenStream> {
let xml_namespace = self.namespace;
let xml_name = self.name;
let attribute_name = self.attribute_name;
let namespace_expr = Expr::Path(ExprPath {
attrs: Vec::new(),
qself: None,
path: xml_namespace.clone(),
});
let builder = Ident::new("builder", Span::call_site());
let mut matchers = quote! {};
for variant in self.variants {
let attribute_value = variant.value;
let path = Path {
leading_colon: None,
segments: [
PathSegment::from(ty_ident.clone()),
PathSegment::from(variant.ident),
]
.into_iter()
.collect(),
};
let (orig_names, mapped_names, map_ident) =
build_ident_mapping(variant.inner.iter_members());
let into_element = variant.inner.build_into_element(
&(Path::from(ty_ident.clone()).into()),
&namespace_expr,
&builder,
map_ident,
)?;
matchers = quote! {
#matchers
#path { #( #orig_names: #mapped_names ),* } => {
let #builder = ::xso::exports::minidom::Element::builder(
#xml_name,
#namespace_expr,
).attr(#attribute_name, #attribute_value);
let #builder = #into_element;
#builder.build()
},
};
}
Ok(matchers)
}
}
/// An enum where each variant has completely independent matching.
#[cfg_attr(feature = "debug", derive(Debug))]
struct Dynamic {
@ -501,6 +766,10 @@ enum EnumInner {
/// Enum item where the variants switch on the XML element's name.
XmlNameSwitched(XmlNameSwitched),
/// Enum item where the variants switch on the value of an attribute on
/// the XML element.
XmlAttributeSwitched(XmlAttributeSwitched),
/// Enum item which has no matcher on the enum itself, where each variant
/// may be an entirely different XML element.
Dynamic(Dynamic),
@ -525,6 +794,9 @@ impl EnumInner {
Self::XmlNameSwitched(inner) => {
inner.build_try_from_element(enum_ident, validate, residual)
}
Self::XmlAttributeSwitched(inner) => {
inner.build_try_from_element(enum_ident, validate, residual)
}
Self::Dynamic(inner) => inner.build_try_from_element(enum_ident, validate, residual),
}
}
@ -537,6 +809,7 @@ impl EnumInner {
fn build_into_element(self, ty_ident: &Ident) -> Result<TokenStream> {
match self {
Self::XmlNameSwitched(inner) => inner.build_into_element(ty_ident),
Self::XmlAttributeSwitched(inner) => inner.build_into_element(ty_ident),
Self::Dynamic(inner) => inner.build_into_element(ty_ident),
}
}
@ -588,6 +861,13 @@ impl EnumDef {
));
}
if let Some(value) = meta.value {
return Err(syn::Error::new_spanned(
value,
"`value` is not allowed on enums",
));
}
let namespace = match meta.namespace {
None => {
// no namespace -> must be dynamic variant. ensure the other
@ -628,20 +908,40 @@ impl EnumDef {
}
};
if let Some(name) = meta.name {
return Err(syn::Error::new_spanned(
name,
"`name` cannot be set on enums (only on variants).",
));
}
let exhaustive = meta.exhaustive;
let name = match meta.name {
None => {
return Ok(Self {
prepare,
validate,
debug,
inner: EnumInner::XmlNameSwitched(XmlNameSwitched::new(
exhaustive, namespace, input,
)?),
});
}
Some(name) => name,
};
let Some(attribute_name) = meta.attribute else {
return Err(syn::Error::new(
meta.span,
"`attribute` option with the name of the XML attribute to match is required for enums matching on attribute value",
));
};
Ok(Self {
prepare,
validate,
debug,
inner: EnumInner::XmlNameSwitched(XmlNameSwitched::new(exhaustive, namespace, input)?),
inner: EnumInner::XmlAttributeSwitched(XmlAttributeSwitched::new(
exhaustive,
namespace,
name.into(),
attribute_name,
input,
)?),
})
}

View File

@ -112,9 +112,9 @@ impl ExtractDef {
quote! { data }
};
let parse = self
.parts
.build_try_from_element(container_name, &namespace_expr, residual)?;
let parse =
self.parts
.build_try_from_element(container_name, &namespace_expr, residual, &[])?;
let test_expr = match self.namespace {
FieldNamespace::Static(xml_namespace) => quote! {

View File

@ -330,6 +330,12 @@ pub(crate) struct XmlCompoundMeta {
/// The value assigned to `name` inside `#[xml(..)]`, if any.
pub(crate) name: Option<NameRef>,
/// The value assigned to `attribute` inside `#[xml(..)]`, if any.
pub(crate) attribute: Option<LitStr>,
/// The value assigned to `value` inside `#[xml(..)]`, if any.
pub(crate) value: Option<LitStr>,
/// Flag indicating the presence of `fallback` inside `#[xml(..)].
pub(crate) fallback: Flag,
@ -357,6 +363,8 @@ impl XmlCompoundMeta {
fn parse_from_attribute(attr: &Attribute) -> Result<Self> {
let mut name: Option<NameRef> = None;
let mut namespace: Option<NamespaceRef> = None;
let mut attribute: Option<LitStr> = None;
let mut value: Option<LitStr> = None;
let mut fallback = Flag::Absent;
let mut transparent = Flag::Absent;
let mut exhaustive = Flag::Absent;
@ -369,6 +377,8 @@ impl XmlCompoundMeta {
meta,
ParseValue("name", &mut name),
ParseValue("namespace", &mut namespace),
ParseValue("attribute", &mut attribute),
ParseValue("value", &mut value),
ParseFlag("fallback", &mut fallback),
ParseFlag("transparent", &mut transparent),
ParseFlag("exhaustive", &mut exhaustive),
@ -390,6 +400,8 @@ impl XmlCompoundMeta {
span: attr.span(),
namespace,
name,
attribute,
value,
fallback,
transparent,
validate,

View File

@ -93,6 +93,8 @@ impl StructInner {
assert!(meta.prepare.is_none());
assert!(!meta.debug.is_set());
assert!(!meta.fallback.is_set());
assert!(meta.attribute.is_none());
assert!(meta.value.is_none());
if let Flag::Present(_) = meta.transparent {
if let Some(namespace) = meta.namespace {
@ -264,7 +266,8 @@ impl StructInner {
path: namespace_tempname.clone().into(),
});
let body = inner.build_try_from_element(struct_name, &namespace_expr, residual)?;
let body =
inner.build_try_from_element(struct_name, &namespace_expr, residual, &[])?;
match namespace {
StructNamespace::Dyn { ty, .. } => Ok(quote! {
@ -421,13 +424,28 @@ impl StructDef {
if let Flag::Present(fallback) = meta.fallback.take() {
return Err(syn::Error::new(
fallback,
"fallback is not allowed on structs",
"`fallback` is not allowed on structs",
));
}
if let Flag::Present(exhaustive) = meta.exhaustive.take() {
return Err(syn::Error::new(
exhaustive,
"exhaustive is not allowed on structs",
"`exhaustive` is not allowed on structs",
));
}
if let Some(attribute) = meta.attribute.take() {
return Err(syn::Error::new_spanned(
attribute,
"`attribute` is not allowed on structs",
));
}
if let Some(value) = meta.value.take() {
return Err(syn::Error::new_spanned(
value,
"`value` is not allowed on structs",
));
}

View File

@ -114,6 +114,11 @@ The following flavors exist:
then specify the name. The variant is picked based on the name of the XML
element.
- XML attribute matched: The enum itself defines a namespace, a name and an
attribute name. Each variant must specify the attribute value. The variant
is picked based on the value of the given attribute, provided that XML name
and namespace of the element itself match.
The flavor is determined based on the attributes of the enum declaration.
### Dynamic enums
@ -154,6 +159,42 @@ XML name matched enum variants support the following attributes:
declared in the `name` attribute of the variant; the original XML name is
lost.
### XML attribute matched enums
XML attribute matched enums support the following attributes:
- `namespace = ..` (required): This must be a path to a `&'static str`. It is
the namespace of the enumeration.
- `name = ..` (required): This must be a string literal containing the XML
name to match against.
- `attribute = ..` (required): This must be a string literal with the name of
the XML attribute to match against.
- `exhaustive` (flag): Must currently be set unless a variant is marked as
`fallback`. Support for non-exhaustive attribute-matched enums is not
implemented yet.
This cannot be used if a variant is set as `fallback`.
This attribute has no relation to the Rust standard `#[non_exhaustive]`
attribute.
#### XML attribute matched enum variants
XML attribute matched enum variants support the following attributes:
- `value = ..` (required): String literal with the attribute value to match
against.
- `fallback` (flag): If present, the variant is parsed when no other variant
matches.
*Note:* When the enum is reserialized to XML, the attribute value will be
the one declared in the `value` attribute of the variant; the original value
is lost.
## Field attributes
Field attributes are composed of a field kind, followed by a value or a list