Repository: tafia/quick-xml
Pull request #298 (2021-07-19 07:21:33)

Implement unflattening structs
This pull gives an (non-ideal) implementation for #245.

Essentially this allows `<root><first>value</first><second>value</second></root>` to be (de)serialized with the special name prefix `$unflatten=`:
```rust,ignore
struct Root {
    #[serde(rename = "$unflatten=first")]
    first: String,
    #[serde(rename = "$unflatten=second")]
    other_field: String,
}
```

Ideally this would be something that could be configured on a struct level, but since from what I understand Serde doesn't allow custom per-implementation attributes... so this uses `rename` as a backchannel, like $value does.

It would be nicer to implement a separate XML trait, but Serde doesn't really like that either, like the comment in #205 knows.
https://github.com/tafia/quick-xml/pull/205#issuecomment-605680462


Patch:
diff --git a/README.md b/README.md
index 058f58f..1dcc713 100644
--- a/README.md
+++ b/README.md
@@ -226,6 +226,20 @@ struct Foo {
 }
 ```
 
+### Unflattening structs into verbose XML
+
+If your XML files look like `<root><first>value</first><second>value</second></root>`, you can
+(de)serialize them with the special name prefix `$unflatten=`:
+
+```rust,ignore
+struct Root {
+    #[serde(rename = "$unflatten=first")]
+    first: String,
+    #[serde(rename = "$unflatten=second")]
+    other_field: String,
+}
+```
+
 ### Performance
 
 Note that despite not focusing on performance (there are several unecessary copies), it remains about 10x faster than serde-xml-rs.
diff --git a/src/de/map.rs b/src/de/map.rs
index 2bce091..7ba1702 100644
--- a/src/de/map.rs
+++ b/src/de/map.rs
@@ -1,7 +1,7 @@
 //! Serde `Deserializer` module
 
 use crate::{
-    de::{escape::EscapedDeserializer, Deserializer, INNER_VALUE},
+    de::{escape::EscapedDeserializer, Deserializer, INNER_VALUE, UNFLATTEN_PREFIX},
     errors::serialize::DeError,
     events::{attributes::Attribute, BytesStart, Event},
 };
@@ -62,6 +62,7 @@ impl<'a, 'de, R: BufRead> de::MapAccess<'de> for MapAccess<'a, R> {
             .map(|a| (a.key.to_owned(), a.value.into_owned()));
         let decoder = self.de.reader.decoder();
         let has_value_field = self.de.has_value_field;
+        let has_unflatten_field = self.de.has_unflatten_field;
         if let Some((key, value)) = attr_key_val {
             // try getting map from attributes (key= "value")
             self.value = MapValue::Attribute { value };
@@ -94,8 +95,15 @@ impl<'a, 'de, R: BufRead> de::MapAccess<'de> for MapAccess<'a, R> {
                     self.value = MapValue::InnerValue;
                     seed.deserialize(INNER_VALUE.into_deserializer()).map(Some)
                 }
+                Some(Event::Start(e)) if has_unflatten_field => {
+                    self.value = MapValue::InnerValue;
+                    let key = format!("{}{}", UNFLATTEN_PREFIX, String::from_utf8(e.local_name().to_vec())
+                                      .expect("$unflatten= did not contain valid Rust identifier"));
+                    seed.deserialize(key.into_deserializer()).map(Some)
+                }
                 Some(Event::Start(e)) => {
                     let name = e.local_name().to_owned();
+
                     self.value = MapValue::Nested;
                     seed.deserialize(EscapedDeserializer::new(name, decoder, false))
                         .map(Some)
diff --git a/src/de/mod.rs b/src/de/mod.rs
index 918c4e9..a4fad67 100644
--- a/src/de/mod.rs
+++ b/src/de/mod.rs
@@ -121,13 +121,15 @@ use serde::de::{self, DeserializeOwned};
 use serde::serde_if_integer128;
 use std::io::BufRead;
 
-const INNER_VALUE: &str = "$value";
+pub(crate) const INNER_VALUE: &str = "$value";
+pub(crate) const UNFLATTEN_PREFIX: &str = "$unflatten=";
 
 /// An xml deserializer
 pub struct Deserializer<R: BufRead> {
     reader: Reader<R>,
     peek: Option<Event<'static>>,
     has_value_field: bool,
+    has_unflatten_field: bool,
 }
 
 /// Deserialize an instance of type T from a string of XML text.
@@ -153,6 +155,7 @@ impl<R: BufRead> Deserializer<R> {
             reader,
             peek: None,
             has_value_field: false,
+            has_unflatten_field: false,
         }
     }
 
@@ -274,9 +277,11 @@ impl<'de, 'a, R: BufRead> de::Deserializer<'de> for &'a mut Deserializer<R> {
         if let Some(e) = self.next_start(&mut Vec::new())? {
             let name = e.name().to_vec();
             self.has_value_field = fields.contains(&INNER_VALUE);
+            self.has_unflatten_field = fields.iter().any(|elem| elem.starts_with(UNFLATTEN_PREFIX));
             let map = map::MapAccess::new(self, e)?;
             let value = visitor.visit_map(map)?;
             self.has_value_field = false;
+            self.has_unflatten_field = false;
             self.read_to_end(&name)?;
             Ok(value)
         } else {
diff --git a/src/se/var.rs b/src/se/var.rs
index d87b4eb..8a7d623 100644
--- a/src/se/var.rs
+++ b/src/se/var.rs
@@ -4,7 +4,9 @@ use crate::{
     se::Serializer,
     writer::Writer,
 };
-use serde::ser::{self, Serialize};
+use de::{INNER_VALUE, UNFLATTEN_PREFIX};
+use serde::ser::{self, Serialize, SerializeMap};
+use serde::Serializer as _;
 use std::io::Write;
 
 /// An implementation of `SerializeMap` for serializing to XML.
@@ -116,17 +118,24 @@ where
     ) -> Result<(), DeError> {
         // TODO: Inherit indentation state from self.parent.writer
         let writer = Writer::new(&mut self.buffer);
-        let mut serializer = Serializer::with_root(writer, Some(key));
-        value.serialize(&mut serializer)?;
-
-        if !self.buffer.is_empty() {
-            if self.buffer[0] == b'<' || key == "$value" {
-                // Drains buffer, moves it to children
-                self.children.append(&mut self.buffer);
-            } else {
-                self.attrs
-                    .push_attribute((key.as_bytes(), self.buffer.as_ref()));
-                self.buffer.clear();
+        if key.starts_with(UNFLATTEN_PREFIX) {
+            let key = key.split_at(UNFLATTEN_PREFIX.len()).1;
+            let mut serializer = Serializer::with_root(writer, Some(key));
+            serializer.serialize_newtype_struct(key, value);
+            self.children.append(&mut self.buffer);
+        } else {
+            let mut serializer = Serializer::with_root(writer, Some(key));
+            value.serialize(&mut serializer)?;
+
+            if !self.buffer.is_empty() {
+                if self.buffer[0] == b'<' || key == INNER_VALUE {
+                    // Drains buffer, moves it to children
+                    self.children.append(&mut self.buffer);
+                } else {
+                    self.attrs
+                        .push_attribute((key.as_bytes(), self.buffer.as_ref()));
+                    self.buffer.clear();
+                }
             }
         }
 


Test patch:
diff --git a/tests/serde_roundtrip.rs b/tests/serde_roundtrip.rs
index 078f0ac..cf0dbb1 100644
--- a/tests/serde_roundtrip.rs
+++ b/tests/serde_roundtrip.rs
@@ -124,6 +124,24 @@ fn no_contiguous_fields() {
     // assert_eq!(serialized, source);
 }
 
+#[test]
+fn test_parse_unflatten_field() {
+    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+    struct Unflatten {
+        #[serde(rename = "$unflatten=NewKey")]
+        field: String
+    }
+
+    let source = "<Unflatten><NewKey>Foo</NewKey></Unflatten>";
+    let expected = Unflatten { field: "Foo".to_string() };
+
+    let parsed: Unflatten = ::quick_xml::de::from_str(source).unwrap();
+    assert_eq!(&parsed, &expected);
+
+    let stringified = to_string(&parsed).unwrap();
+    assert_eq!(&stringified, source);
+}
+
 #[test]
 fn escapes_in_cdata() {
     #[derive(Debug, Deserialize, PartialEq)]