Skip to content

Commit 621da10

Browse files
authored
Merge pull request #594 from Kriskras99/master
Add documentation for tagged enums with Serde
2 parents 88a2f27 + 858118e commit 621da10

File tree

3 files changed

+215
-0
lines changed

3 files changed

+215
-0
lines changed

Changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626

2727
### Misc Changes
2828

29+
- [#594]: Add a helper macro to help deserialize internally tagged enums
30+
with Serde, which doesn't work out-of-box due to serde limitations.
31+
2932
[#581]: https://github.com/tafia/quick-xml/pull/581
33+
[#594]: https://github.com/tafia/quick-xml/pull/594
3034
[#601]: https://github.com/tafia/quick-xml/pull/601
3135
[#603]: https://github.com/tafia/quick-xml/pull/603
3236
[#606]: https://github.com/tafia/quick-xml/pull/606

src/de/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
//! - [Frequently Used Patterns](#frequently-used-patterns)
2929
//! - [`<element>` lists](#element-lists)
3030
//! - [Enum::Unit Variants As a Text](#enumunit-variants-as-a-text)
31+
//! - [Internally Tagged Enums](#internally-tagged-enums)
3132
//!
3233
//!
3334
//!
@@ -1743,10 +1744,28 @@
17431744
//! If you still want to keep your struct untouched, you can instead use the
17441745
//! helper module [`text_content`].
17451746
//!
1747+
//!
1748+
//! Internally Tagged Enums
1749+
//! -----------------------
1750+
//! [Tagged enums] are currently not supported because of an issue in the Serde
1751+
//! design (see [serde#1183] and [quick-xml#586]) and missing optimizations in
1752+
//! serde which could be useful for XML case ([serde#1495]). This can be worked
1753+
//! around by manually implementing deserialize with `#[serde(deserialize_with = "func")]`
1754+
//! or implementing [`Deserialize`], but this can get very tedious very fast for
1755+
//! files with large amounts of tagged enums. To help with this issue the quick-xml
1756+
//! provides a macro [`impl_deserialize_for_internally_tagged_enum!`]. See the
1757+
//! macro documentation for details.
1758+
//!
1759+
//!
17461760
//! [specification]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition
17471761
//! [`deserialize_with`]: https://serde.rs/field-attrs.html#deserialize_with
17481762
//! [#497]: https://github.com/tafia/quick-xml/issues/497
17491763
//! [`text_content`]: crate::serde_helpers::text_content
1764+
//! [Tagged enums]: https://serde.rs/enum-representations.html#internally-tagged
1765+
//! [serde#1183]: https://github.com/serde-rs/serde/issues/1183
1766+
//! [serde#1495]: https://github.com/serde-rs/serde/issues/1495
1767+
//! [quick-xml#586]: https://github.com/tafia/quick-xml/issues/586
1768+
//! [`impl_deserialize_for_internally_tagged_enum!`]: crate::impl_deserialize_for_internally_tagged_enum
17501769
17511770
// Macros should be defined before the modules that using them
17521771
// Also, macros should be imported before using them

src/serde_helpers.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,198 @@
22
33
use serde::{Deserialize, Deserializer, Serialize, Serializer};
44

5+
#[macro_export]
6+
#[doc(hidden)]
7+
macro_rules! deserialize_variant {
8+
// Produce struct enum variant
9+
( $de:expr, $enum:tt, $variant:ident {
10+
$(
11+
$(#[$meta:meta])*
12+
$field:ident : $typ:ty
13+
),* $(,)?
14+
} ) => ({
15+
let var = {
16+
// Create anonymous type
17+
#[derive(serde::Deserialize)]
18+
struct $variant {
19+
$(
20+
$(#[$meta])*
21+
$field: $typ,
22+
)*
23+
}
24+
<$variant>::deserialize($de)?
25+
};
26+
// Due to https://github.com/rust-lang/rust/issues/86935 we cannot use
27+
// <$enum> :: $variant
28+
use $enum :: *;
29+
$variant {
30+
$($field: var.$field,)*
31+
}
32+
});
33+
34+
// Produce newtype enum variant
35+
( $de:expr, $enum:tt, $variant:ident($typ:ty) ) => ({
36+
let var = <$typ>::deserialize($de)?;
37+
<$enum> :: $variant(var)
38+
});
39+
40+
// Produce unit enum variant
41+
( $de:expr, $enum:tt, $variant:ident ) => ({
42+
serde::de::IgnoredAny::deserialize($de)?;
43+
<$enum> :: $variant
44+
});
45+
}
46+
47+
/// A helper to implement [`Deserialize`] for [internally tagged] enums which
48+
/// does not use [`Deserializer::deserialize_any`] that produces wrong results
49+
/// with XML because of [serde#1183].
50+
///
51+
/// In contract to deriving [`Deserialize`] this macro assumes that a tag will be
52+
/// the first element or attribute in the XML.
53+
///
54+
/// # Example
55+
///
56+
/// ```
57+
/// # use pretty_assertions::assert_eq;
58+
/// use quick_xml::de::from_str;
59+
/// use quick_xml::impl_deserialize_for_internally_tagged_enum;
60+
/// use serde::Deserialize;
61+
///
62+
/// #[derive(Deserialize, Debug, PartialEq)]
63+
/// struct Root {
64+
/// one: InternallyTaggedEnum,
65+
/// two: InternallyTaggedEnum,
66+
/// three: InternallyTaggedEnum,
67+
/// }
68+
///
69+
/// #[derive(Debug, PartialEq)]
70+
/// // #[serde(tag = "@tag")]
71+
/// enum InternallyTaggedEnum {
72+
/// Unit,
73+
/// Newtype(Newtype),
74+
/// Struct {
75+
/// // #[serde(rename = "@attribute")]
76+
/// attribute: u32,
77+
/// element: f32,
78+
/// },
79+
/// }
80+
///
81+
/// #[derive(Deserialize, Debug, PartialEq)]
82+
/// struct Newtype {
83+
/// #[serde(rename = "@attribute")]
84+
/// attribute: u64,
85+
/// }
86+
///
87+
/// // The macro needs the type of the enum, the tag name,
88+
/// // and information about all the variants
89+
/// impl_deserialize_for_internally_tagged_enum!{
90+
/// InternallyTaggedEnum, "@tag",
91+
/// ("Unit" => Unit),
92+
/// ("Newtype" => Newtype(Newtype)),
93+
/// ("Struct" => Struct {
94+
/// #[serde(rename = "@attribute")]
95+
/// attribute: u32,
96+
/// element: f32,
97+
/// }),
98+
/// }
99+
///
100+
/// assert_eq!(
101+
/// from_str::<Root>(r#"
102+
/// <root>
103+
/// <one tag="Unit" />
104+
/// <two tag="Newtype" attribute="42" />
105+
/// <three tag="Struct" attribute="42">
106+
/// <element>4.2</element>
107+
/// </three>
108+
/// </root>
109+
/// "#).unwrap(),
110+
/// Root {
111+
/// one: InternallyTaggedEnum::Unit,
112+
/// two: InternallyTaggedEnum::Newtype(Newtype { attribute: 42 }),
113+
/// three: InternallyTaggedEnum::Struct {
114+
/// attribute: 42,
115+
/// element: 4.2,
116+
/// },
117+
/// },
118+
/// );
119+
/// ```
120+
///
121+
/// [internally tagged]: https://serde.rs/enum-representations.html#internally-tagged
122+
/// [serde#1183]: https://github.com/serde-rs/serde/issues/1183
123+
#[macro_export(local_inner_macros)]
124+
macro_rules! impl_deserialize_for_internally_tagged_enum {
125+
(
126+
$enum:ty,
127+
$tag:literal,
128+
$(
129+
($variant_tag:literal => $($variant:tt)+ )
130+
),* $(,)?
131+
) => {
132+
impl<'de> serde::de::Deserialize<'de> for $enum {
133+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134+
where
135+
D: serde::de::Deserializer<'de>,
136+
{
137+
use serde::de::{Error, MapAccess, Visitor};
138+
139+
// The Visitor struct is normally used for state, but none is needed
140+
struct TheVisitor;
141+
// The main logic of the deserializing happens in the Visitor trait
142+
impl<'de> Visitor<'de> for TheVisitor {
143+
// The type that is being deserialized
144+
type Value = $enum;
145+
146+
// Try to give a better error message when this is used wrong
147+
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
148+
f.write_str("expecting map with tag in ")?;
149+
f.write_str($tag)
150+
}
151+
152+
// The xml data is provided as an opaque map,
153+
// that map is parsed into the type
154+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
155+
where
156+
A: MapAccess<'de>,
157+
{
158+
// Here the assumption is made that only one attribute
159+
// exists and it's the discriminator (enum "tag").
160+
let entry: Option<(String, String)> = map.next_entry()?;
161+
// If there are more attributes those would need
162+
// to be parsed as well.
163+
let tag = match entry {
164+
// Return an error if the no attributes are found,
165+
// and indicate that the @tag attribute is missing.
166+
None => Err(A::Error::missing_field($tag)),
167+
// Check if the attribute is the tag
168+
Some((attribute, value)) => {
169+
if attribute == $tag {
170+
// return the value of the tag
171+
Ok(value)
172+
} else {
173+
// The attribute is not @tag, return an error
174+
// indicating that there is an unexpected attribute
175+
Err(A::Error::unknown_field(&attribute, &[$tag]))
176+
}
177+
}
178+
}?;
179+
180+
let de = serde::de::value::MapAccessDeserializer::new(map);
181+
match tag.as_ref() {
182+
$(
183+
$variant_tag => Ok(deserialize_variant!( de, $enum, $($variant)+ )),
184+
)*
185+
_ => Err(A::Error::unknown_field(&tag, &[$($variant_tag),+])),
186+
}
187+
}
188+
}
189+
// Tell the deserializer to deserialize the data as a map,
190+
// using the TheVisitor as the decoder
191+
deserializer.deserialize_map(TheVisitor)
192+
}
193+
}
194+
}
195+
}
196+
5197
/// Provides helper functions to serialization and deserialization of types
6198
/// (usually enums) as a text content of an element and intended to use with
7199
/// [`#[serde(with = "...")]`][with], [`#[serde(deserialize_with = "...")]`][de-with]

0 commit comments

Comments
 (0)