Repository: tafia/quick-xml
Pull request #221 (2020-06-27 07:48:29)

Serialize basic type as attributes when using serde
As [this issue](https://github.com/tafia/quick-xml/issues/196) said, basic type struct fields should be serialized to attributes.

Rather than check type name, I check the serialization result of the field. When the result is a tag(starting with '<'), I treat it as a child node.

In serialize_field(), I don't serialize the field to final result, but store attrs and child nodes in separate temporary location. After all fields have been collected, in end(), I will write them to final result. So the field order is not important.

I got `test framework quit unexpectedly` while runing test in serde_roundtrip.rs, so I write tests in another project. Here is my tests:

```rust
use serde::*;
use quick_xml::se::to_string;
use regex::Regex;
use std::borrow::Cow;

#[derive(Serialize)]
#[serde(rename="classroom")]
struct Classroom {
    pub students: Vec<Person>,
    pub number: String,
    pub adviser: Person
}

#[derive(Serialize)]
#[serde(rename="person")]
struct Person {
    pub name: String,
    pub age: u32
}

#[derive(Serialize)]
#[serde(rename="empty")]
struct Empty {}

#[test]
fn test_nested() {
    let s1 = Person { name: "sherlock".to_string(), age: 20 };
    let s2 = Person { name: "harry".to_string(), age: 19 };
    let t = Person { name: "albus".to_string(), age: 88 };
    let doc = Classroom { students: vec![s1, s2], number: "3-1".to_string(), adviser: t };
    let xml = quick_xml::se::to_string(&doc).unwrap();

    let str = r#"<classroom number="3-1">
                        <students>
                          <person name="sherlock" age="20"></person>
                          <person name="harry" age="19"></person>
                        </students>
                        <adviser>
                          <person name="albus" age="88"></person>
                        </adviser>
                      </classroom>"#;
    assert_eq!(xml, inline(str));
}

fn inline(str: &str) -> Cow<str> {
    let regex = Regex::new(r">\s+<").unwrap();
    regex.replace_all(str, "><")
}

#[test]
fn test_empty() {
    let e = Empty {};
    let xml = to_string(&e).unwrap();
    assert_eq!(xml, "<empty></empty>");
}
```


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index b407255..401e7a6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ memchr = "2.3.3"
 
 [dev-dependencies]
 serde = { version = "1.0", features = ["derive"] }
+regex = "1"
 
 [lib]
 bench = false
diff --git a/src/events/attributes.rs b/src/events/attributes.rs
index 095d430..bcdffdb 100644
--- a/src/events/attributes.rs
+++ b/src/events/attributes.rs
@@ -295,7 +295,7 @@ impl<'a> Iterator for Attributes<'a> {
                 .consumed
                 .iter()
                 .filter(|r| r.len() == end_key - start_key)
-                .find(|r| &self.bytes[(*r).clone()] == &self.bytes[start_key..end_key])
+                .find(|r| self.bytes[(*r).clone()] == self.bytes[start_key..end_key])
                 .map(|ref r| r.start)
             {
                 err!(Error::DuplicatedAttribute(start_key, start));
diff --git a/src/events/mod.rs b/src/events/mod.rs
index 107207b..761832a 100644
--- a/src/events/mod.rs
+++ b/src/events/mod.rs
@@ -45,7 +45,7 @@ impl<'a> BytesStart<'a> {
     pub fn borrowed(content: &'a [u8], name_len: usize) -> Self {
         BytesStart {
             buf: Cow::Borrowed(content),
-            name_len: name_len,
+            name_len,
         }
     }
 
@@ -66,7 +66,7 @@ impl<'a> BytesStart<'a> {
     pub fn owned<C: Into<Vec<u8>>>(content: C, name_len: usize) -> BytesStart<'static> {
         BytesStart {
             buf: Cow::Owned(content.into()),
-            name_len: name_len,
+            name_len,
         }
     }
 
diff --git a/src/reader.rs b/src/reader.rs
index 092d45a..8d04a37 100644
--- a/src/reader.rs
+++ b/src/reader.rs
@@ -97,7 +97,7 @@ impl<B: BufRead> Reader<B> {
     /// Creates a `Reader` that reads from a reader implementing `BufRead`.
     pub fn from_reader(reader: B) -> Reader<B> {
         Reader {
-            reader: reader,
+            reader,
             opened_buffer: Vec::new(),
             opened_starts: Vec::new(),
             tag_state: TagState::Closed,
@@ -339,7 +339,7 @@ impl<B: BufRead> Reader<B> {
                         return Err(Error::UnexpectedEof("Comment".to_string()));
                     }
                     Ok(_) => (),
-                    Err(e) => return Err(e.into()),
+                    Err(e) => return Err(e),
                 }
             }
             let len = buf.len();
@@ -394,11 +394,11 @@ impl<B: BufRead> Reader<B> {
                         &buf[buf_start + 8..buf.len()],
                     )))
                 }
-                _ => return Err(Error::UnexpectedBang),
+                _ => Err(Error::UnexpectedBang),
             }
         } else {
             self.buf_position -= buf.len() - buf_start;
-            return Err(Error::UnexpectedBang);
+            Err(Error::UnexpectedBang)
         }
     }
 
@@ -1144,10 +1144,10 @@ impl NamespaceBufferIndex {
                             let start = buffer.len();
                             buffer.extend_from_slice(&*v);
                             self.slices.push(Namespace {
-                                start: start,
+                                start,
                                 prefix_len: 0,
                                 value_len: v.len(),
-                                level: level,
+                                level,
                             });
                         }
                         Some(&b':') => {
@@ -1155,10 +1155,10 @@ impl NamespaceBufferIndex {
                             buffer.extend_from_slice(&k[6..]);
                             buffer.extend_from_slice(&*v);
                             self.slices.push(Namespace {
-                                start: start,
+                                start,
                                 prefix_len: k.len() - 6,
                                 value_len: v.len(),
-                                level: level,
+                                level,
                             });
                         }
                         _ => break,
diff --git a/src/se/mod.rs b/src/se/mod.rs
index 07d900f..2b89e65 100644
--- a/src/se/mod.rs
+++ b/src/se/mod.rs
@@ -209,8 +209,9 @@ impl<'w, W: Write> ser::Serializer for &'w mut Serializer<W> {
         name: &'static str,
         _len: usize,
     ) -> Result<Self::SerializeStruct, DeError> {
-        self.writer
-            .write_event(Event::Start(BytesStart::borrowed_name(name.as_bytes())))?;
+        write!(self.writer.inner(), "<{}", name).map_err(|err| {
+            DeError::Custom(format!("serialize struct {} failed: {}", name, err))
+        })?;
         Ok(Struct::new(self, name))
     }
 
@@ -258,7 +259,7 @@ mod tests {
         }
 
         let got = String::from_utf8(buffer).unwrap();
-        assert_eq!(got, "<foo>");
+        assert_eq!(got, "<foo");
     }
 
     #[test]
@@ -269,10 +270,13 @@ mod tests {
             let mut ser = Serializer::new(&mut buffer);
             let mut struct_ser = Struct::new(&mut ser, "baz");
             struct_ser.serialize_field("foo", "bar").unwrap();
+            let attrs = std::str::from_utf8(&struct_ser.attrs).unwrap();
+            assert_eq!(attrs, " foo=\"bar\"");
+            let _ = struct_ser.end().unwrap();
         }
 
         let got = String::from_utf8(buffer).unwrap();
-        assert_eq!(got, "<foo>bar</foo>");
+        assert_eq!(got, " foo=\"bar\"></baz>");
     }
 
     #[test]
@@ -287,7 +291,7 @@ mod tests {
             name: "Bob".to_string(),
             age: 42,
         };
-        let should_be = "<Person><name>Bob</name><age>42</age></Person>";
+        let should_be = "<Person name=\"Bob\" age=\"42\"></Person>";
         let mut buffer = Vec::new();
 
         {
diff --git a/src/se/var.rs b/src/se/var.rs
index a77b544..dd4eb33 100644
--- a/src/se/var.rs
+++ b/src/se/var.rs
@@ -1,6 +1,6 @@
 use crate::{
     errors::{serialize::DeError, Error},
-    events::{BytesEnd, BytesStart, Event},
+    events::{BytesEnd, Event},
     se::Serializer,
 };
 use serde::ser::{self, Serialize};
@@ -72,6 +72,9 @@ where
 {
     parent: &'w mut Serializer<W>,
     name: &'w str,
+    pub(crate) attrs: Vec<u8>,
+    children: Vec<u8>,
+    buffer: Vec<u8>,
 }
 
 impl<'w, W> Struct<'w, W>
@@ -80,7 +83,7 @@ where
 {
     /// Create a new `Struct`
     pub fn new(parent: &'w mut Serializer<W>, name: &'w str) -> Struct<'w, W> {
-        Struct { parent, name }
+        Struct { parent, name, attrs: Vec::new(), children: Vec::new(), buffer: Vec::new() }
     }
 }
 
@@ -96,18 +99,36 @@ where
         key: &'static str,
         value: &T,
     ) -> Result<(), DeError> {
-        let key = key.as_bytes();
-        self.parent
-            .writer
-            .write_event(Event::Start(BytesStart::borrowed_name(key)))?;
-        value.serialize(&mut *self.parent)?;
-        self.parent
-            .writer
-            .write_event(Event::End(BytesEnd::borrowed(key)))?;
+        let mut serializer = Serializer::new(&mut self.buffer);
+        value.serialize(&mut serializer)?;
+
+        if !self.buffer.is_empty() {
+            if self.buffer[0] == b'<' {
+                write!(&mut self.children, "<{}>", key).map_err(Error::Io)?;
+                self.children.extend(&self.buffer);
+                write!(&mut self.children, "</{}>", key).map_err(Error::Io)?;
+            } else {
+                write!(&mut self.attrs, " {}=\"", key).map_err(Error::Io)?;
+                self.attrs.extend(&self.buffer);
+                write!(&mut self.attrs, "\"").map_err(Error::Io)?;
+            }
+
+            self.buffer.clear();
+        }
+
         Ok(())
     }
 
     fn end(self) -> Result<Self::Ok, DeError> {
+        self.parent
+            .writer
+            .write(&self.attrs)?;
+        self.parent
+            .writer
+            .write(">".as_bytes())?;
+        self.parent
+            .writer
+            .write(&self.children)?;
         self.parent
             .writer
             .write_event(Event::End(BytesEnd::borrowed(self.name.as_bytes())))?;
diff --git a/src/writer.rs b/src/writer.rs
index c9dc4fd..939057a 100644
--- a/src/writer.rs
+++ b/src/writer.rs
@@ -190,7 +190,7 @@ impl Indentation {
     }
 
     fn grow(&mut self) {
-        self.indents_len = self.indents_len + self.indent_size;
+        self.indents_len += self.indent_size;
         if self.indents_len > self.indents.len() {
             self.indents.resize(self.indents_len, self.indent_char);
         }


Test patch:
diff --git a/tests/serde_attrs.rs b/tests/serde_attrs.rs
new file mode 100644
index 0000000..32ca657
--- /dev/null
+++ b/tests/serde_attrs.rs
@@ -0,0 +1,61 @@
+#![cfg(feature = "serialize")]
+
+extern crate quick_xml;
+extern crate serde;
+extern crate regex;
+
+use quick_xml::se::to_string;
+use serde::Serialize;
+use regex::Regex;
+use std::borrow::Cow;
+
+#[derive(Serialize)]
+#[serde(rename="classroom")]
+struct Classroom {
+    pub students: Vec<Person>,
+    pub number: String,
+    pub adviser: Person
+}
+
+#[derive(Serialize)]
+#[serde(rename="person")]
+struct Person {
+    pub name: String,
+    pub age: u32
+}
+
+#[derive(Serialize)]
+#[serde(rename="empty")]
+struct Empty {}
+
+#[test]
+fn test_nested() {
+    let s1 = Person { name: "sherlock".to_string(), age: 20 };
+    let s2 = Person { name: "harry".to_string(), age: 19 };
+    let t = Person { name: "albus".to_string(), age: 88 };
+    let doc = Classroom { students: vec![s1, s2], number: "3-1".to_string(), adviser: t };
+    let xml = quick_xml::se::to_string(&doc).unwrap();
+
+    let str = r#"<classroom number="3-1">
+                   <students>
+                      <person name="sherlock" age="20"></person>
+                      <person name="harry" age="19"></person>
+                   </students>
+                   <adviser>
+                     <person name="albus" age="88"></person>
+                   </adviser>
+                 </classroom>"#;
+    assert_eq!(xml, inline(str));
+}
+
+fn inline(str: &str) -> Cow<str> {
+    let regex = Regex::new(r">\s+<").unwrap();
+    regex.replace_all(str, "><")
+}
+
+#[test]
+fn test_empty() {
+    let e = Empty {};
+    let xml = to_string(&e).unwrap();
+    assert_eq!(xml, "<empty></empty>");
+}
diff --git a/tests/serde_roundtrip.rs b/tests/serde_roundtrip.rs
index 4b175cd..b68b2d9 100644
--- a/tests/serde_roundtrip.rs
+++ b/tests/serde_roundtrip.rs
@@ -37,6 +37,7 @@ fn basic_struct() {
     assert_eq!(item, should_be);
 
     let reserialized_item = to_string(&item).unwrap();
+    let src = "<Item name=\"Banana\" source=\"Store\"></Item>";
     assert_eq!(src, reserialized_item);
 }