Repository: tafia/quick-xml
Pull request #541 (2023-01-07 21:39:59)

Implement deserialization textual content into externally tagged enum variants
This is partially address #257. This PR adds an ability to use specially named enum variant `$text`, which would get a textual content:
```rust
enum Choice {
  One,
  Two,
  #[serde(rename = "$text")]
  Text(String),
}
```
is able to deserialize
```xml
<one>...</one>
text
<two>...</two>
```
into `Vec<Choice>`:
```rust
vec![
  Choice::One,
  Choice::Text("text"),
  Choice::Two,
]
```


codecov-commenter: # [Codecov](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#541](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (ac3c3d5) into [master](https://codecov.io/gh/tafia/quick-xml/commit/967d60dffdf23aa9c34c7d85ec3575a4cadc45cb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (967d60d) will **increase** coverage by `0.36%`.
> The diff coverage is `48.52%`.

```diff
@@            Coverage Diff             @@
##           master     #541      +/-   ##
==========================================
+ Coverage   60.91%   61.27%   +0.36%     
==========================================
  Files          33       32       -1     
  Lines       15722    15639      -83     
==========================================
+ Hits         9577     9583       +6     
+ Misses       6145     6056      -89     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `61.27% <48.52%> (+0.36%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [src/de/mod.rs](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2RlL21vZC5ycw==) | `56.86% <3.33%> (-0.53%)` | :arrow_down: |
| [src/de/var.rs](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2RlL3Zhci5ycw==) | `91.54% <84.21%> (+6.44%)` | :arrow_up: |
| [src/events/mod.rs](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2V2ZW50cy9tb2QucnM=) | `71.64% <0.00%> (-0.75%)` | :arrow_down: |
| [src/name.rs](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL25hbWUucnM=) | `87.82% <0.00%> (-0.72%)` | :arrow_down: |
| [src/escapei.rs](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2VzY2FwZWkucnM=) | `12.41% <0.00%> (-0.18%)` | :arrow_down: |
| [src/de/simple\_type.rs](https://codecov.io/gh/tafia/quick-xml/pull/541?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2RlL3NpbXBsZV90eXBlLnJz) | `93.65% <0.00%> (+0.77%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/Changelog.md b/Changelog.md
index 8b30ab0..d22f496 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -12,6 +12,9 @@
 
 ### New Features
 
+- [#541]: Deserialize specially named `$text` enum variant in [externally tagged]
+  enums from textual content
+
 ### Bug Fixes
 
 - [#537]: Restore ability to deserialize attributes that represents XML namespace
@@ -19,8 +22,10 @@
 
 ### Misc Changes
 
+[externally tagged]: https://serde.rs/enum-representations.html#externally-tagged
 [#490]: https://github.com/tafia/quick-xml/pull/490
 [#537]: https://github.com/tafia/quick-xml/issues/537
+[#541]: https://github.com/tafia/quick-xml/pull/541
 
 ## 0.27.1 -- 2022-12-28
 
diff --git a/src/de/escape.rs b/src/de/escape.rs
deleted file mode 100644
index 446b1d2..0000000
--- a/src/de/escape.rs
+++ /dev/null
@@ -1,223 +0,0 @@
-//! Serde `Deserializer` module
-
-use crate::de::deserialize_bool;
-use crate::encoding::Decoder;
-use crate::errors::serialize::DeError;
-use crate::escape::unescape;
-use serde::de::{DeserializeSeed, EnumAccess, VariantAccess, Visitor};
-use serde::{self, forward_to_deserialize_any, serde_if_integer128};
-use std::borrow::Cow;
-
-/// A deserializer for a xml escaped and encoded value
-///
-/// # Note
-///
-/// Escaping the value is actually not always necessary, for instance
-/// when converting to float, we don't expect any escapable character
-/// anyway
-#[derive(Clone, Debug)]
-pub struct EscapedDeserializer<'a> {
-    decoder: Decoder,
-    /// Possible escaped value of text/CDATA or attribute value
-    escaped_value: Cow<'a, [u8]>,
-    /// If `true`, value requires unescaping before using
-    escaped: bool,
-}
-
-impl<'a> EscapedDeserializer<'a> {
-    pub fn new(escaped_value: Cow<'a, [u8]>, decoder: Decoder, escaped: bool) -> Self {
-        EscapedDeserializer {
-            decoder,
-            escaped_value,
-            escaped,
-        }
-    }
-}
-
-macro_rules! deserialize_num {
-    ($method:ident, $visit:ident) => {
-        fn $method<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-        where
-            V: Visitor<'de>,
-        {
-            let value = self.decoder.decode(self.escaped_value.as_ref())?.parse()?;
-
-            visitor.$visit(value)
-        }
-    };
-}
-
-impl<'de, 'a> serde::Deserializer<'de> for EscapedDeserializer<'a> {
-    type Error = DeError;
-
-    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        self.deserialize_str(visitor)
-    }
-
-    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        let decoded = self.decoder.decode(&self.escaped_value)?;
-        if self.escaped {
-            match unescape(&decoded)? {
-                Cow::Borrowed(s) => visitor.visit_str(s),
-                Cow::Owned(s) => visitor.visit_string(s),
-            }
-        } else {
-            match decoded {
-                Cow::Borrowed(s) => visitor.visit_str(s),
-                Cow::Owned(s) => visitor.visit_string(s),
-            }
-        }
-    }
-
-    /// Returns [`DeError::Unsupported`]
-    fn deserialize_bytes<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        Err(DeError::Unsupported(
-            "binary data content is not supported by XML format".into(),
-        ))
-    }
-
-    /// Forwards deserialization to the [`deserialize_bytes`](#method.deserialize_bytes)
-    fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        self.deserialize_bytes(visitor)
-    }
-
-    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        self.deserialize_str(visitor)
-    }
-
-    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        deserialize_bool(self.escaped_value.as_ref(), self.decoder, visitor)
-    }
-
-    fn deserialize_char<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        self.deserialize_str(visitor)
-    }
-
-    fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        visitor.visit_unit()
-    }
-
-    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        if self.escaped_value.as_ref().is_empty() {
-            visitor.visit_none()
-        } else {
-            visitor.visit_some(self)
-        }
-    }
-
-    fn deserialize_enum<V>(
-        self,
-        _name: &str,
-        _variants: &'static [&'static str],
-        visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        visitor.visit_enum(self)
-    }
-
-    fn deserialize_newtype_struct<V>(
-        self,
-        _name: &'static str,
-        visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        visitor.visit_newtype_struct(self)
-    }
-
-    deserialize_num!(deserialize_i64, visit_i64);
-    deserialize_num!(deserialize_i32, visit_i32);
-    deserialize_num!(deserialize_i16, visit_i16);
-    deserialize_num!(deserialize_i8, visit_i8);
-    deserialize_num!(deserialize_u64, visit_u64);
-    deserialize_num!(deserialize_u32, visit_u32);
-    deserialize_num!(deserialize_u16, visit_u16);
-    deserialize_num!(deserialize_u8, visit_u8);
-    deserialize_num!(deserialize_f64, visit_f64);
-    deserialize_num!(deserialize_f32, visit_f32);
-
-    serde_if_integer128! {
-        deserialize_num!(deserialize_i128, visit_i128);
-        deserialize_num!(deserialize_u128, visit_u128);
-    }
-
-    forward_to_deserialize_any! {
-        unit_struct seq tuple tuple_struct map struct identifier ignored_any
-    }
-}
-
-impl<'de, 'a> EnumAccess<'de> for EscapedDeserializer<'a> {
-    type Error = DeError;
-    type Variant = Self;
-
-    fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self), Self::Error>
-    where
-        V: DeserializeSeed<'de>,
-    {
-        let name = seed.deserialize(self.clone())?;
-        Ok((name, self))
-    }
-}
-
-impl<'de, 'a> VariantAccess<'de> for EscapedDeserializer<'a> {
-    type Error = DeError;
-
-    fn unit_variant(self) -> Result<(), Self::Error> {
-        Ok(())
-    }
-
-    fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
-    where
-        T: DeserializeSeed<'de>,
-    {
-        seed.deserialize(self)
-    }
-
-    fn tuple_variant<V>(self, _len: usize, _visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        unimplemented!()
-    }
-
-    fn struct_variant<V>(
-        self,
-        _fields: &'static [&'static str],
-        _visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: Visitor<'de>,
-    {
-        unimplemented!()
-    }
-}
diff --git a/src/de/mod.rs b/src/de/mod.rs
index abd3925..199bb99 100644
--- a/src/de/mod.rs
+++ b/src/de/mod.rs
@@ -473,7 +473,7 @@
 //! <!-- 8 ===================================================================================== -->
 //! <tr>
 //! <td>
-//! An XML with different root tag names:
+//! An XML with different root tag names, as well as text / CDATA content:
 //!
 //! ```xml
 //! <one field1="...">...</one>
@@ -483,12 +483,18 @@
 //!   <field2>...</field2>
 //! </two>
 //! ```
+//! ```xml
+//! Text <![CDATA[or (mixed)
+//! CDATA]]> content
+//! ```
 //! </td>
 //! <td>
 //!
 //! An enum where each variant have a name of the possible root tag. The name of
 //! the enum itself does not matter.
 //!
+//! If you need to get a textual content, mark a variant with `#[serde(rename = "$text")]`.
+//!
 //! All these structs can be used to deserialize from any XML on the
 //! left side depending on amount of information that you want to get:
 //!
@@ -503,9 +509,19 @@
 //! enum AnyName {
 //!   One { #[serde(rename = "@field1")] field1: T },
 //!   Two { field2: U },
+//!
+//!   /// Use unit variant, if you do not care of a content.
+//!   /// You can use tuple variant if you want to parse
+//!   /// textual content as an xs:list.
+//!   /// Struct variants are not supported and will return
+//!   /// Err(Unsupported)
+//!   #[serde(rename = "$text")]
+//!   Text(String),
 //! }
 //! # assert_eq!(AnyName::One { field1: () }, quick_xml::de::from_str(r#"<one field1="...">...</one>"#).unwrap());
 //! # assert_eq!(AnyName::Two { field2: () }, quick_xml::de::from_str(r#"<two><field2>...</field2></two>"#).unwrap());
+//! # assert_eq!(AnyName::Text("text".into()), quick_xml::de::from_str(r#"text"#).unwrap());
+//! # // TODO: After #474 parse mixed content
 //! ```
 //! ```
 //! # use pretty_assertions::assert_eq;
@@ -523,9 +539,13 @@
 //!   // `field1` content discarded
 //!   One,
 //!   Two(Two),
+//!   #[serde(rename = "$text")]
+//!   Text,
 //! }
 //! # assert_eq!(AnyName::One,                     quick_xml::de::from_str(r#"<one field1="...">...</one>"#).unwrap());
 //! # assert_eq!(AnyName::Two(Two { field2: () }), quick_xml::de::from_str(r#"<two><field2>...</field2></two>"#).unwrap());
+//! # assert_eq!(AnyName::Text,                    quick_xml::de::from_str(r#"text"#).unwrap());
+//! # // TODO: After #474 parse mixed content
 //! ```
 //! ```
 //! # use pretty_assertions::assert_eq;
@@ -535,12 +555,14 @@
 //! #[serde(rename_all = "snake_case")]
 //! enum AnyName {
 //!   One,
-//!   // the <two> will be mapped to this
+//!   // the <two> and textual content will be mapped to this
 //!   #[serde(other)]
 //!   Other,
 //! }
 //! # assert_eq!(AnyName::One,   quick_xml::de::from_str(r#"<one field1="...">...</one>"#).unwrap());
 //! # assert_eq!(AnyName::Other, quick_xml::de::from_str(r#"<two><field2>...</field2></two>"#).unwrap());
+//! # assert_eq!(AnyName::Other, quick_xml::de::from_str(r#"text"#).unwrap());
+//! # // TODO: After #474 parse mixed content
 //! ```
 //! <div style="background:rgba(120,145,255,0.45);padding:0.75em;">
 //!
@@ -569,11 +591,19 @@
 //!   <two>...</two>
 //! </any-tag>
 //! ```
+//! ```xml
+//! <any-tag field="...">
+//!   Text <![CDATA[or (mixed)
+//!   CDATA]]> content
+//! </any-tag>
+//! ```
 //! </td>
 //! <td>
 //!
 //! A structure with a field which type is an `enum`.
 //!
+//! If you need to get a textual content, mark a variant with `#[serde(rename = "$text")]`.
+//!
 //! Names of the enum, struct, and struct field with `Choice` type does not matter:
 //!
 //! ```
@@ -586,6 +616,14 @@
 //! enum Choice {
 //!   One,
 //!   Two,
+//!
+//!   /// Use unit variant, if you do not care of a content.
+//!   /// You can use tuple variant if you want to parse
+//!   /// textual content as an xs:list.
+//!   /// Struct variants are not supported and will return
+//!   /// Err(Unsupported)
+//!   #[serde(rename = "$text")]
+//!   Text(String),
 //! }
 //! # #[derive(Debug, PartialEq)]
 //! #[derive(Deserialize)]
@@ -604,6 +642,11 @@
 //! #   AnyName { field: (), any_name: Choice::Two },
 //! #   quick_xml::de::from_str(r#"<any-tag field="..."><two>...</two></any-tag>"#).unwrap(),
 //! # );
+//! # assert_eq!(
+//! #   AnyName { field: (), any_name: Choice::Text("text".into()) },
+//! #   // TODO: After #474 parse mixed content
+//! #   quick_xml::de::from_str(r#"<any-tag field="...">text</any-tag>"#).unwrap(),
+//! # );
 //! ```
 //! </td>
 //! </tr>
@@ -1023,8 +1066,7 @@
 //! # );
 //! ```
 //! ```ignore
-//! // FIXME: Custom("unknown variant `text`, expected
-//! //                one of `one`, `two`, `$value`")
+//! // FIXME: #474
 //! # use pretty_assertions::assert_eq;
 //! # use serde::Deserialize;
 //! # #[derive(Debug, PartialEq)]
@@ -1033,7 +1075,7 @@
 //! enum Choice {
 //!   One,
 //!   Two,
-//!   #[serde(rename = "$value")]
+//!   #[serde(rename = "$text")]
 //!   Other(String),
 //! }
 //! type AnyName = Vec<Choice>;
@@ -1202,8 +1244,7 @@
 //! You MUST specify `#[serde(rename = "$value")]` on that field:
 //!
 //! ```ignore
-//! // FIXME: Custom("unknown variant `text`, expected
-//! //                one of `one`, `two`, `$value`")
+//! // FIXME: #474
 //! # use pretty_assertions::assert_eq;
 //! # use serde::Deserialize;
 //! # #[derive(Debug, PartialEq)]
@@ -1212,7 +1253,7 @@
 //! enum Choice {
 //!   One,
 //!   Two,
-//!   #[serde(rename = "$value")]
+//!   #[serde(rename = "$text")]
 //!   Other(String),
 //! }
 //! # #[derive(Debug, PartialEq)]
@@ -1248,8 +1289,7 @@
 //! # );
 //! ```
 //! ```ignore
-//! // FIXME: Custom("unknown variant `text`, expected
-//! //                one of `one`, `two`, `$value`")
+//! // FIXME: #474
 //! # use pretty_assertions::assert_eq;
 //! # use serde::Deserialize;
 //! # #[derive(Debug, PartialEq)]
@@ -1258,7 +1298,7 @@
 //! enum Choice {
 //!   One,
 //!   Two,
-//!   #[serde(rename = "$value")]
+//!   #[serde(rename = "$text")]
 //!   Other(String),
 //! }
 //! # #[derive(Debug, PartialEq)]
@@ -1792,7 +1832,6 @@ macro_rules! deserialize_primitives {
     };
 }
 
-mod escape;
 mod key;
 mod map;
 mod simple_type;
@@ -2434,8 +2473,7 @@ where
     where
         V: Visitor<'de>,
     {
-        let value = visitor.visit_enum(var::EnumAccess::new(self))?;
-        Ok(value)
+        visitor.visit_enum(var::EnumAccess::new(self))
     }
 
     fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, DeError>
diff --git a/src/de/var.rs b/src/de/var.rs
index 2034ced..8a7b019 100644
--- a/src/de/var.rs
+++ b/src/de/var.rs
@@ -1,9 +1,11 @@
 use crate::{
-    de::{escape::EscapedDeserializer, DeEvent, Deserializer, XmlRead},
+    de::key::QNameDeserializer,
+    de::simple_type::SimpleTypeDeserializer,
+    de::{DeEvent, Deserializer, XmlRead, TEXT_KEY},
     errors::serialize::DeError,
 };
-use serde::de::{self, DeserializeSeed, Deserializer as SerdeDeserializer, Visitor};
-use std::borrow::Cow;
+use serde::de::value::StrDeserializer;
+use serde::de::{self, DeserializeSeed, Deserializer as _, Visitor};
 
 /// An enum access
 pub struct EnumAccess<'de, 'a, R>
@@ -34,21 +36,25 @@ where
         V: DeserializeSeed<'de>,
     {
         let decoder = self.de.reader.decoder();
-        let de = match self.de.peek()? {
-            DeEvent::Text(t) => EscapedDeserializer::new(Cow::Borrowed(t), decoder, true),
-            // Escape sequences does not processed inside CDATA section
-            DeEvent::CData(t) => EscapedDeserializer::new(Cow::Borrowed(t), decoder, false),
-            DeEvent::Start(e) => {
-                EscapedDeserializer::new(Cow::Borrowed(e.local_name().into_inner()), decoder, false)
-            }
-            _ => {
-                return Err(DeError::Unsupported(
-                    "Invalid event for Enum, expecting `Text` or `Start`".into(),
-                ))
-            }
+        let (name, is_text) = match self.de.peek()? {
+            DeEvent::Start(e) => (
+                seed.deserialize(QNameDeserializer::from_elem(e.name(), decoder)?)?,
+                false,
+            ),
+            DeEvent::Text(_) | DeEvent::CData(_) => (
+                seed.deserialize(StrDeserializer::<DeError>::new(TEXT_KEY))?,
+                true,
+            ),
+            DeEvent::End(e) => return Err(DeError::UnexpectedEnd(e.name().into_inner().to_vec())),
+            DeEvent::Eof => return Err(DeError::UnexpectedEof),
         };
-        let name = seed.deserialize(de)?;
-        Ok((name, VariantAccess { de: self.de }))
+        Ok((
+            name,
+            VariantAccess {
+                de: self.de,
+                is_text,
+            },
+        ))
     }
 }
 
@@ -57,6 +63,9 @@ where
     R: XmlRead<'de>,
 {
     de: &'a mut Deserializer<'de, R>,
+    /// `true` if variant should be deserialized from a textual content
+    /// and `false` if from tag
+    is_text: bool,
 }
 
 impl<'de, 'a, R> de::VariantAccess<'de> for VariantAccess<'de, 'a, R>
@@ -67,9 +76,13 @@ where
 
     fn unit_variant(self) -> Result<(), DeError> {
         match self.de.next()? {
+            // Consume subtree
             DeEvent::Start(e) => self.de.read_to_end(e.name()),
+            // Does not needed to deserialize using SimpleTypeDeserializer, because
+            // it returns `()` when `deserialize_unit()` is requested
             DeEvent::Text(_) | DeEvent::CData(_) => Ok(()),
-            _ => unreachable!(),
+            // SAFETY: the other events are filtered in `variant_seed()`
+            _ => unreachable!("Only `Start`, `Text` or `CData` events are possible here"),
         }
     }
 
@@ -77,14 +90,38 @@ where
     where
         T: DeserializeSeed<'de>,
     {
-        seed.deserialize(&mut *self.de)
+        if self.is_text {
+            match self.de.next()? {
+                DeEvent::Text(e) => {
+                    seed.deserialize(SimpleTypeDeserializer::from_text_content(e.decode(true)?))
+                }
+                DeEvent::CData(e) => {
+                    seed.deserialize(SimpleTypeDeserializer::from_text_content(e.decode()?))
+                }
+                // SAFETY: the other events are filtered in `variant_seed()`
+                _ => unreachable!("Only `Text` or `CData` events are possible here"),
+            }
+        } else {
+            seed.deserialize(&mut *self.de)
+        }
     }
 
     fn tuple_variant<V>(self, len: usize, visitor: V) -> Result<V::Value, DeError>
     where
         V: Visitor<'de>,
     {
-        self.de.deserialize_tuple(len, visitor)
+        if self.is_text {
+            match self.de.next()? {
+                DeEvent::Text(e) => SimpleTypeDeserializer::from_text_content(e.decode(true)?)
+                    .deserialize_tuple(len, visitor),
+                DeEvent::CData(e) => SimpleTypeDeserializer::from_text_content(e.decode()?)
+                    .deserialize_tuple(len, visitor),
+                // SAFETY: the other events are filtered in `variant_seed()`
+                _ => unreachable!("Only `Text` or `CData` events are possible here"),
+            }
+        } else {
+            self.de.deserialize_tuple(len, visitor)
+        }
     }
 
     fn struct_variant<V>(
@@ -95,6 +132,17 @@ where
     where
         V: Visitor<'de>,
     {
-        self.de.deserialize_struct("", fields, visitor)
+        if self.is_text {
+            match self.de.next()? {
+                DeEvent::Text(e) => SimpleTypeDeserializer::from_text_content(e.decode(true)?)
+                    .deserialize_struct("", fields, visitor),
+                DeEvent::CData(e) => SimpleTypeDeserializer::from_text_content(e.decode()?)
+                    .deserialize_struct("", fields, visitor),
+                // SAFETY: the other events are filtered in `variant_seed()`
+                _ => unreachable!("Only `Text` or `CData` events are possible here"),
+            }
+        } else {
+            self.de.deserialize_struct("", fields, visitor)
+        }
     }
 }


Test patch:
diff --git a/tests/serde-de.rs b/tests/serde-de.rs
index 050a6dd..f78c212 100644
--- a/tests/serde-de.rs
+++ b/tests/serde-de.rs
@@ -5071,6 +5071,8 @@ mod enum_ {
         float: String,
     }
 
+    /// Enum tag selector is a name of the `<element>` or text / CDATA content
+    /// for a `$text` variant
     mod externally_tagged {
         use super::*;
         use pretty_assertions::assert_eq;
@@ -5269,8 +5271,142 @@ mod enum_ {
                 );
             }
         }
+
+        /// Test deserialization of the specially named variant `$text`
+        mod text {
+            use super::*;
+
+            mod unit {
+                use super::*;
+                use pretty_assertions::assert_eq;
+
+                #[derive(Debug, Deserialize, PartialEq)]
+                enum Text {
+                    #[serde(rename = "$text")]
+                    Unit,
+                }
+
+                #[test]
+                fn text() {
+                    let data: Text = from_str(" text ").unwrap();
+                    assert_eq!(data, Text::Unit);
+                }
+
+                #[test]
+                fn cdata() {
+                    let data: Text = from_str("<![CDATA[ cdata ]]>").unwrap();
+                    assert_eq!(data, Text::Unit);
+                }
+
+                #[test]
+                #[ignore = "awaiting fix of https://github.com/tafia/quick-xml/issues/474"]
+                fn mixed() {
+                    let data: Text = from_str(" te <![CDATA[ cdata ]]> xt ").unwrap();
+                    assert_eq!(data, Text::Unit);
+                }
+            }
+
+            mod newtype {
+                use super::*;
+                use pretty_assertions::assert_eq;
+
+                #[derive(Debug, Deserialize, PartialEq)]
+                enum Text {
+                    #[serde(rename = "$text")]
+                    Newtype(String),
+                }
+
+                #[test]
+                fn text() {
+                    let data: Text = from_str(" text ").unwrap();
+                    assert_eq!(data, Text::Newtype("text".into()));
+                }
+
+                #[test]
+                fn cdata() {
+                    let data: Text = from_str("<![CDATA[ cdata ]]>").unwrap();
+                    assert_eq!(data, Text::Newtype(" cdata ".into()));
+                }
+
+                #[test]
+                #[ignore = "awaiting fix of https://github.com/tafia/quick-xml/issues/474"]
+                fn mixed() {
+                    let data: Text = from_str(" te <![CDATA[ cdata ]]> xt ").unwrap();
+                    assert_eq!(data, Text::Newtype("te  cdata  xt".into()));
+                }
+            }
+
+            /// Tuple variant deserialized as an `xs:list`, that is why spaces
+            /// are trimmed even in CDATA sections
+            mod tuple {
+                use super::*;
+                use pretty_assertions::assert_eq;
+
+                #[derive(Debug, Deserialize, PartialEq)]
+                enum Text {
+                    #[serde(rename = "$text")]
+                    Tuple(f64, String),
+                }
+
+                #[test]
+                fn text() {
+                    let data: Text = from_str(" 4.2 text ").unwrap();
+                    assert_eq!(data, Text::Tuple(4.2, "text".into()));
+                }
+
+                #[test]
+                fn cdata() {
+                    let data: Text = from_str("<![CDATA[ 4.2 cdata ]]>").unwrap();
+                    assert_eq!(data, Text::Tuple(4.2, "cdata".into()));
+                }
+
+                #[test]
+                #[ignore = "awaiting fix of https://github.com/tafia/quick-xml/issues/474"]
+                fn mixed() {
+                    let data: Text = from_str(" 4.2 <![CDATA[ cdata ]]>").unwrap();
+                    assert_eq!(data, Text::Tuple(4.2, "cdata".into()));
+                }
+            }
+
+            /// Struct variant cannot be directly deserialized from `Text` / `CData` events
+            mod struct_ {
+                use super::*;
+
+                #[derive(Debug, Deserialize, PartialEq)]
+                enum Text {
+                    #[serde(rename = "$text")]
+                    Struct { float: f64, string: String },
+                }
+
+                #[test]
+                fn text() {
+                    match from_str::<Text>(" text ") {
+                        Err(DeError::Unsupported(_)) => {}
+                        x => panic!("Expected `Err(Unsupported(_))`, but found {:?}", x),
+                    }
+                }
+
+                #[test]
+                fn cdata() {
+                    match from_str::<Text>("<![CDATA[ cdata ]]>") {
+                        Err(DeError::Unsupported(_)) => {}
+                        x => panic!("Expected `Err(Unsupported(_))`, but found {:?}", x),
+                    }
+                }
+
+                #[test]
+                fn mixed() {
+                    match from_str::<Text>(" te <![CDATA[ cdata ]]> xt ") {
+                        Err(DeError::Unsupported(_)) => {}
+                        x => panic!("Expected `Err(Unsupported(_))`, but found {:?}", x),
+                    }
+                }
+            }
+        }
     }
 
+    /// Enum tag selector either an attribute "tag", or a tag "tag".
+    /// `$text` variant could be defined, but that name has no special meaning
     mod internally_tagged {
         use super::*;
 
@@ -5368,7 +5504,7 @@ mod enum_ {
             fn attributes() {
                 let data: NodeAttr = from_str(
                     // Comment for prevent unnecessary formatting - we use the same style in all tests
-                    r#"<root tag="Newtype" value="true"/>"#,
+                    r#"<root tag="Newtype"><value>true</value></root>"#,
                 )
                 .unwrap();
                 assert_eq!(data, NodeAttr::Newtype(NewtypeContent { value: true }));
@@ -5486,6 +5622,8 @@ mod enum_ {
         }
     }
 
+    /// Enum tag selector either an attribute "tag", or a tag "tag".
+    /// `$text` variant could be defined, but that name has no special meaning
     mod adjacently_tagged {
         use super::*;
 
@@ -5743,6 +5881,8 @@ mod enum_ {
         }
     }
 
+    /// Enum tags does not exist.
+    /// `$text` variant could be defined, but that name has no special meaning
     mod untagged {
         use super::*;
         use pretty_assertions::assert_eq;
diff --git a/tests/serde-migrated.rs b/tests/serde-migrated.rs
index 4fdcb9b..6033d16 100644
--- a/tests/serde-migrated.rs
+++ b/tests/serde-migrated.rs
@@ -117,80 +117,6 @@ fn test_parse_string_not_trim() {
     test_parse_ok(&[("<bla>     </bla>", "     ".to_string())]);
 }
 
-#[test]
-#[ignore] // FIXME
-fn test_parse_enum() {
-    use Animal::*;
-
-    #[derive(PartialEq, Debug, Serialize, Deserialize)]
-    enum Animal {
-        Dog,
-        Frog(String),
-        Ant(Simple),
-        Cat { age: usize, name: String },
-    }
-
-    test_parse_ok(&[
-        ("<Animal xsi:type=\"Dog\"/>", Dog),
-        (
-            "<Animal xsi:type=\"Frog\">Quak</Animal>",
-            Frog("Quak".to_string()),
-        ),
-        (
-            "<Animal xsi:type=\"Ant\"><a/><c>bla</c><b>15</b><d>Foo</d></Animal>",
-            Ant(Simple {
-                a: (),
-                b: 15,
-                c: "bla".to_string(),
-                d: Some("Foo".to_string()),
-            }),
-        ),
-        (
-            "<Animal xsi:type=\"Ant\"><a/><c>bla</c><b>15</b></Animal>",
-            Ant(Simple {
-                a: (),
-                b: 15,
-                c: "bla".to_string(),
-                d: None,
-            }),
-        ),
-        (
-            "<Animal xsi:type=\"Cat\"><age>42</age><name>Shere Khan</name></Animal>",
-            Cat {
-                age: 42,
-                name: "Shere Khan".to_string(),
-            },
-        ),
-    ]);
-
-    #[derive(PartialEq, Debug, Serialize, Deserialize)]
-    struct Helper {
-        x: Animal,
-    }
-
-    test_parse_ok(&[
-        ("<Helper><x xsi:type=\"Dog\"/></Helper>", Helper { x: Dog }),
-        (
-            "<Helper><x xsi:type=\"Frog\">Quak</Animal></Helper>",
-            Helper {
-                x: Frog("Quak".to_string()),
-            },
-        ),
-        (
-            "<Helper><x xsi:type=\"Cat\">
-                <age>42</age>
-                <name>Shere Khan</name>
-            </x></Helper>",
-            Helper {
-                x: Cat {
-                    age: 42,
-                    name: "Shere Khan".to_string(),
-                },
-            },
-        ),
-    ]);
-}
-
 #[test]
 fn test_option() {
     test_parse_ok(&[