From bd0b792dc65f4e747d76c5c8745e95dde7e58679 Mon Sep 17 00:00:00 2001 From: Mingun Date: Fri, 9 Sep 2022 20:58:37 +0500 Subject: [PATCH] Implement indentation for new serializer. Fix #361 Still 2 doc-test failures --- src/de/simple_type.rs | 3 +- src/se/content.rs | 261 ++++++++++- src/se/element.rs | 1041 ++++++++++++++++++++++++++++++++++++++++- src/se/mod.rs | 52 +- src/se/simple_type.rs | 39 +- src/writer.rs | 25 +- 6 files changed, 1380 insertions(+), 41 deletions(-) diff --git a/src/de/simple_type.rs b/src/de/simple_type.rs index 5181f869..d7e4ca3a 100644 --- a/src/de/simple_type.rs +++ b/src/de/simple_type.rs @@ -829,7 +829,7 @@ impl<'de> VariantAccess<'de> for SimpleTypeUnitOnly { mod tests { use super::*; use crate::se::simple_type::{QuoteTarget, SimpleTypeSerializer}; - use crate::se::QuoteLevel; + use crate::se::{Indent, QuoteLevel}; use crate::utils::{ByteBuf, Bytes}; use serde::de::IgnoredAny; use serde::{Deserialize, Serialize}; @@ -866,6 +866,7 @@ mod tests { writer: String::new(), target: QuoteTarget::Text, level: QuoteLevel::Full, + indent: Indent::None, }) .unwrap(), xml diff --git a/src/se/content.rs b/src/se/content.rs index 6f756fb9..4a1a97c9 100644 --- a/src/se/content.rs +++ b/src/se/content.rs @@ -3,7 +3,7 @@ use crate::errors::serialize::DeError; use crate::se::element::{ElementSerializer, Struct}; use crate::se::simple_type::{QuoteTarget, SimpleTypeSerializer}; -use crate::se::{QuoteLevel, XmlName}; +use crate::se::{Indent, QuoteLevel, XmlName}; use serde::ser::{ Impossible, Serialize, SerializeSeq, SerializeTuple, SerializeTupleStruct, Serializer, }; @@ -39,23 +39,39 @@ macro_rules! write_primitive { /// sequences and maps are serialized. Unlike `SimpleTypeSerializer` it supports /// any types in sequences and serializes them as list of elements, but that has /// drawbacks. Sequence of primitives would be serialized without delimiters and -/// it will be impossible to distinguish between them. -pub struct ContentSerializer { +/// it will be impossible to distinguish between them. Even worse, when serializing +/// with indent, sequence of strings become one big string with additional content +/// and it would be impossible to distinguish between content of the original +/// strings and inserted indent characters. +pub struct ContentSerializer<'i, W: Write> { pub writer: W, /// Defines which XML characters need to be escaped in text content pub level: QuoteLevel, + /// Current indentation level. Note, that `Indent::None` means that there is + /// no indentation at all, but `write_indent == false` means only, that indent + /// writing is disabled in this instantiation of `ContentSerializer`, but + /// child serializers should have access to the actual state of indentation. + pub(super) indent: Indent<'i>, + /// If `true`, then current indent will be written before writing the content, + /// but only if content is not empty. + pub write_indent: bool, //TODO: add settings to disallow consequent serialization of primitives } -impl ContentSerializer { +impl<'i, W: Write> ContentSerializer<'i, W> { /// Turns this serializer into serializer of a text content #[inline] - pub fn into_simple_type_serializer(self) -> SimpleTypeSerializer { + pub fn into_simple_type_serializer(self) -> SimpleTypeSerializer<'i, W> { //TODO: Customization point: choose between CDATA and Text representation SimpleTypeSerializer { writer: self.writer, target: QuoteTarget::Text, level: self.level, + indent: if self.write_indent { + self.indent + } else { + Indent::None + }, } } @@ -66,12 +82,15 @@ impl ContentSerializer { ContentSerializer { writer: &mut self.writer, level: self.level, + indent: self.indent.borrow(), + write_indent: self.write_indent, } } /// Writes `name` as self-closed tag #[inline] pub(super) fn write_empty(mut self, name: XmlName) -> Result { + self.write_indent()?; self.writer.write_char('<')?; self.writer.write_str(name.0)?; self.writer.write_str("/>")?; @@ -81,8 +100,9 @@ impl ContentSerializer { /// Writes simple type content between `name` tags pub(super) fn write_wrapped(mut self, name: XmlName, serialize: S) -> Result where - S: FnOnce(SimpleTypeSerializer) -> Result, + S: FnOnce(SimpleTypeSerializer<'i, W>) -> Result, { + self.write_indent()?; self.writer.write_char('<')?; self.writer.write_str(name.0)?; self.writer.write_char('>')?; @@ -94,19 +114,27 @@ impl ContentSerializer { writer.write_char('>')?; Ok(writer) } + + pub(super) fn write_indent(&mut self) -> Result<(), DeError> { + if self.write_indent { + self.indent.write_indent(&mut self.writer)?; + self.write_indent = false; + } + Ok(()) + } } -impl Serializer for ContentSerializer { +impl<'i, W: Write> Serializer for ContentSerializer<'i, W> { type Ok = W; type Error = DeError; type SerializeSeq = Self; type SerializeTuple = Self; type SerializeTupleStruct = Self; - type SerializeTupleVariant = ElementSerializer<'static, W>; + type SerializeTupleVariant = ElementSerializer<'i, W>; type SerializeMap = Impossible; type SerializeStruct = Impossible; - type SerializeStructVariant = Struct<'static, W>; + type SerializeStructVariant = Struct<'i, W>; write_primitive!(serialize_bool(bool)); @@ -129,9 +157,17 @@ impl Serializer for ContentSerializer { write_primitive!(serialize_f64(f64)); write_primitive!(serialize_char(char)); - write_primitive!(serialize_str(&str)); write_primitive!(serialize_bytes(&[u8])); + #[inline] + fn serialize_str(self, value: &str) -> Result { + if value.is_empty() { + Ok(self.writer) + } else { + self.into_simple_type_serializer().serialize_str(value) + } + } + /// Does not write anything #[inline] fn serialize_none(self) -> Result { @@ -261,7 +297,7 @@ impl Serializer for ContentSerializer { } } -impl SerializeSeq for ContentSerializer { +impl<'i, W: Write> SerializeSeq for ContentSerializer<'i, W> { type Ok = W; type Error = DeError; @@ -270,6 +306,8 @@ impl SerializeSeq for ContentSerializer { T: ?Sized + Serialize, { value.serialize(self.new_seq_element_serializer())?; + // Write indent for next element + self.write_indent = true; Ok(()) } @@ -279,7 +317,7 @@ impl SerializeSeq for ContentSerializer { } } -impl SerializeTuple for ContentSerializer { +impl<'i, W: Write> SerializeTuple for ContentSerializer<'i, W> { type Ok = W; type Error = DeError; @@ -297,7 +335,7 @@ impl SerializeTuple for ContentSerializer { } } -impl SerializeTupleStruct for ContentSerializer { +impl<'i, W: Write> SerializeTupleStruct for ContentSerializer<'i, W> { type Ok = W; type Error = DeError; @@ -442,6 +480,8 @@ pub(super) mod tests { let ser = ContentSerializer { writer: String::new(), level: QuoteLevel::Full, + indent: Indent::None, + write_indent: false, }; let buffer = $data.serialize(ser).unwrap(); @@ -460,6 +500,8 @@ pub(super) mod tests { let ser = ContentSerializer { writer: &mut buffer, level: QuoteLevel::Full, + indent: Indent::None, + write_indent: false, }; match $data.serialize(ser).unwrap_err() { @@ -611,4 +653,197 @@ pub(super) mod tests { => r#"answer"#); } } + + mod with_indent { + use super::Struct; + use super::*; + use crate::writer::Indentation; + use pretty_assertions::assert_eq; + + /// Checks that given `$data` successfully serialized as `$expected` + macro_rules! serialize_as { + ($name:ident: $data:expr => $expected:literal) => { + #[test] + fn $name() { + let ser = ContentSerializer { + writer: String::new(), + level: QuoteLevel::Full, + indent: Indent::Owned(Indentation::new(b' ', 2)), + write_indent: false, + }; + + let buffer = $data.serialize(ser).unwrap(); + assert_eq!(buffer, $expected); + } + }; + } + + /// Checks that attempt to serialize given `$data` results to a + /// serialization error `$kind` with `$reason` + macro_rules! err { + ($name:ident: $data:expr => $kind:ident($reason:literal)) => { + #[test] + fn $name() { + let mut buffer = String::new(); + let ser = ContentSerializer { + writer: &mut buffer, + level: QuoteLevel::Full, + indent: Indent::Owned(Indentation::new(b' ', 2)), + write_indent: false, + }; + + match $data.serialize(ser).unwrap_err() { + DeError::$kind(e) => assert_eq!(e, $reason), + e => panic!( + "Expected `{}({})`, found `{:?}`", + stringify!($kind), + $reason, + e + ), + } + // We can write something before fail + // assert_eq!(buffer, ""); + } + }; + } + + serialize_as!(false_: false => "false"); + serialize_as!(true_: true => "true"); + + serialize_as!(i8_: -42i8 => "-42"); + serialize_as!(i16_: -4200i16 => "-4200"); + serialize_as!(i32_: -42000000i32 => "-42000000"); + serialize_as!(i64_: -42000000000000i64 => "-42000000000000"); + serialize_as!(isize_: -42000000000000isize => "-42000000000000"); + + serialize_as!(u8_: 42u8 => "42"); + serialize_as!(u16_: 4200u16 => "4200"); + serialize_as!(u32_: 42000000u32 => "42000000"); + serialize_as!(u64_: 42000000000000u64 => "42000000000000"); + serialize_as!(usize_: 42000000000000usize => "42000000000000"); + + serde_if_integer128! { + serialize_as!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); + serialize_as!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + } + + serialize_as!(f32_: 4.2f32 => "4.2"); + serialize_as!(f64_: 4.2f64 => "4.2"); + + serialize_as!(char_non_escaped: 'h' => "h"); + serialize_as!(char_lt: '<' => "<"); + serialize_as!(char_gt: '>' => ">"); + serialize_as!(char_amp: '&' => "&"); + serialize_as!(char_apos: '\'' => "'"); + serialize_as!(char_quot: '"' => """); + //TODO: add a setting to escape leading/trailing spaces, in order to + // pretty-print does not change the content + serialize_as!(char_space: ' ' => " "); + + serialize_as!(str_non_escaped: "non-escaped string" => "non-escaped string"); + serialize_as!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + + err!(bytes: Bytes(b"<\"escaped & bytes'>") => Unsupported("`serialize_bytes` not supported yet")); + + serialize_as!(option_none: Option::::None => ""); + serialize_as!(option_some: Some(Enum::Unit) => ""); + + serialize_as!(unit: () => ""); + serialize_as!(unit_struct: Unit => ""); + serialize_as!(unit_struct_escaped: UnitEscaped => ""); + + // Unlike SimpleTypeSerializer, enumeration values serialized as tags + serialize_as!(enum_unit: Enum::Unit => ""); + err!(enum_unit_escaped: Enum::UnitEscaped + => Unsupported("character `<` is not allowed at the start of an XML name `<\"&'>`")); + + // Newtypes recursively applies ContentSerializer + serialize_as!(newtype: Newtype(42) => "42"); + serialize_as!(enum_newtype: Enum::Newtype(42) => "42"); + + // Note that sequences of primitives serialized without delimiters other that indent! + serialize_as!(seq: vec![1, 2, 3] + => "1\n\ + 2\n\ + 3"); + serialize_as!(seq_empty: Vec::::new() => ""); + serialize_as!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) + => "<"&'>\n\ + with\t\r\n spaces\n\ + 3"); + serialize_as!(tuple_struct: Tuple("first", 42) + => "first\n\ + 42"); + serialize_as!(enum_tuple: Enum::Tuple("first", 42) + => "first\n\ + 42"); + + // Structured types cannot be serialized without surrounding tag, which + // only `enum` can provide + err!(map: BTreeMap::from([("_1", 2), ("_3", 4)]) + => Unsupported("serialization of map types is not supported in `$value` field")); + err!(struct_: Struct { key: "answer", val: (42, 42) } + => Unsupported("serialization of struct `Struct` is not supported in `$value` field")); + serialize_as!(enum_struct: Enum::Struct { key: "answer", val: (42, 42) } + => "\n \ + answer\n \ + 42\n \ + 42\n\ + "); + + /// Special field name `$text` should be serialized as text content + mod text { + use super::*; + use pretty_assertions::assert_eq; + + err!(map: BTreeMap::from([("$text", 2), ("_3", 4)]) + => Unsupported("serialization of map types is not supported in `$value` field")); + err!(struct_: + Text { + before: "answer", + content: (42, 42), + after: "answer", + } + => Unsupported("serialization of struct `Text` is not supported in `$value` field")); + serialize_as!(enum_struct: + SpecialEnum::Text { + before: "answer", + content: (42, 42), + after: "answer", + } + => "\n \ + answer\n \ + 42 42\n \ + answer\n\ + "); + } + + mod attributes { + use super::*; + use pretty_assertions::assert_eq; + + err!(map_attr: BTreeMap::from([("@key1", 1), ("@key2", 2)]) + => Unsupported("serialization of map types is not supported in `$value` field")); + err!(map_mixed: BTreeMap::from([("@key1", 1), ("key2", 2)]) + => Unsupported("serialization of map types is not supported in `$value` field")); + + err!(struct_: Attributes { key: "answer", val: (42, 42) } + => Unsupported("serialization of struct `Attributes` is not supported in `$value` field")); + err!(struct_before: AttributesBefore { key: "answer", val: 42 } + => Unsupported("serialization of struct `AttributesBefore` is not supported in `$value` field")); + err!(struct_after: AttributesAfter { key: "answer", val: 42 } + => Unsupported("serialization of struct `AttributesAfter` is not supported in `$value` field")); + + serialize_as!(enum_: Enum::Attributes { key: "answer", val: (42, 42) } + => r#""#); + serialize_as!(enum_before: Enum::AttributesBefore { key: "answer", val: 42 } + => "\n \ + 42\n\ + "); + serialize_as!(enum_after: Enum::AttributesAfter { key: "answer", val: 42 } + => "\n \ + answer\n\ + "); + } + } } diff --git a/src/se/element.rs b/src/se/element.rs index a94485c4..57033290 100644 --- a/src/se/element.rs +++ b/src/se/element.rs @@ -5,7 +5,7 @@ use crate::errors::serialize::DeError; use crate::se::content::ContentSerializer; use crate::se::key::QNameSerializer; use crate::se::simple_type::{QuoteTarget, SimpleTypeSerializer}; -use crate::se::XmlName; +use crate::se::{Indent, XmlName}; use serde::ser::{ Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, Serializer, @@ -25,7 +25,7 @@ macro_rules! write_primitive { /// A serializer used to serialize element with specified name. pub struct ElementSerializer<'k, W: Write> { - pub ser: ContentSerializer, + pub ser: ContentSerializer<'k, W>, /// Tag name used to wrap serialized types except enum variants which uses the variant name pub(super) key: XmlName<'k>, } @@ -176,6 +176,9 @@ impl<'k, W: Write> Serializer for ElementSerializer<'k, W> { _name: &'static str, _len: usize, ) -> Result { + self.ser.write_indent()?; + self.ser.indent.increase(); + self.ser.writer.write_char('<')?; self.ser.writer.write_str(self.key.0)?; Ok(Struct { @@ -209,6 +212,8 @@ impl<'k, W: Write> SerializeSeq for ElementSerializer<'k, W> { ser: self.ser.new_seq_element_serializer(), key: self.key, })?; + // Write indent for next element + self.ser.write_indent = true; Ok(()) } @@ -322,6 +327,7 @@ impl<'k, W: Write> Struct<'k, W> { writer: &mut self.ser.ser.writer, target: QuoteTarget::DoubleQAttr, level: self.ser.ser.level, + indent: Indent::None, })?; self.ser.ser.writer.write_char('"')?; @@ -346,6 +352,8 @@ impl<'k, W: Write> Struct<'k, W> { let ser = ContentSerializer { writer: &mut self.children, level: self.ser.ser.level, + indent: self.ser.ser.indent.borrow(), + write_indent: true, }; if key == TEXT_KEY { @@ -374,11 +382,16 @@ impl<'k, W: Write> SerializeStruct for Struct<'k, W> { } fn end(mut self) -> Result { + self.ser.ser.indent.decrease(); + if self.children.is_empty() { self.ser.ser.writer.write_str("/>")?; } else { self.ser.ser.writer.write_char('>')?; self.ser.ser.writer.write_str(&self.children)?; + + self.ser.ser.indent.write_indent(&mut self.ser.ser.writer)?; + self.ser.ser.writer.write_str("')?; @@ -479,7 +492,7 @@ impl<'k, W: Write> SerializeMap for Map<'k, W> { mod tests { use super::*; use crate::se::content::tests::*; - use crate::se::QuoteLevel; + use crate::se::{Indent, QuoteLevel}; use crate::utils::Bytes; use serde::Serialize; use std::collections::BTreeMap; @@ -515,6 +528,8 @@ mod tests { ser: ContentSerializer { writer: String::new(), level: QuoteLevel::Full, + indent: Indent::None, + write_indent: false, }, key: XmlName("root"), }; @@ -536,6 +551,8 @@ mod tests { ser: ContentSerializer { writer: &mut buffer, level: QuoteLevel::Full, + indent: Indent::None, + write_indent: false, }, key: XmlName("root"), }; @@ -1453,4 +1470,1022 @@ mod tests { "); } } + + mod with_indent { + use super::*; + use crate::se::content::tests::Struct; + use crate::writer::Indentation; + use pretty_assertions::assert_eq; + + /// Checks that given `$data` successfully serialized as `$expected` + macro_rules! serialize_as { + ($name:ident: $data:expr => $expected:expr) => { + #[test] + fn $name() { + let ser = ElementSerializer { + ser: ContentSerializer { + writer: String::new(), + level: QuoteLevel::Full, + indent: Indent::Owned(Indentation::new(b' ', 2)), + write_indent: false, + }, + key: XmlName("root"), + }; + + let buffer = $data.serialize(ser).unwrap(); + assert_eq!(buffer, $expected); + } + }; + } + + /// Checks that attempt to serialize given `$data` results to a + /// serialization error `$kind` with `$reason` + macro_rules! err { + ($name:ident: $data:expr => $kind:ident($reason:literal)) => { + #[test] + fn $name() { + let mut buffer = String::new(); + let ser = ElementSerializer { + ser: ContentSerializer { + writer: &mut buffer, + level: QuoteLevel::Full, + indent: Indent::Owned(Indentation::new(b' ', 2)), + write_indent: false, + }, + key: XmlName("root"), + }; + + match $data.serialize(ser).unwrap_err() { + DeError::$kind(e) => assert_eq!(e, $reason), + e => panic!( + "Expected `{}({})`, found `{:?}`", + stringify!($kind), + $reason, + e + ), + } + // We can write something before fail + // assert_eq!(buffer, ""); + } + }; + } + + serialize_as!(false_: false => "false"); + serialize_as!(true_: true => "true"); + + serialize_as!(i8_: -42i8 => "-42"); + serialize_as!(i16_: -4200i16 => "-4200"); + serialize_as!(i32_: -42000000i32 => "-42000000"); + serialize_as!(i64_: -42000000000000i64 => "-42000000000000"); + serialize_as!(isize_: -42000000000000isize => "-42000000000000"); + + serialize_as!(u8_: 42u8 => "42"); + serialize_as!(u16_: 4200u16 => "4200"); + serialize_as!(u32_: 42000000u32 => "42000000"); + serialize_as!(u64_: 42000000000000u64 => "42000000000000"); + serialize_as!(usize_: 42000000000000usize => "42000000000000"); + + serde_if_integer128! { + serialize_as!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); + serialize_as!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + } + + serialize_as!(f32_: 4.2f32 => "4.2"); + serialize_as!(f64_: 4.2f64 => "4.2"); + + serialize_as!(char_non_escaped: 'h' => "h"); + serialize_as!(char_lt: '<' => "<"); + serialize_as!(char_gt: '>' => ">"); + serialize_as!(char_amp: '&' => "&"); + serialize_as!(char_apos: '\'' => "'"); + serialize_as!(char_quot: '"' => """); + //TODO: add a setting to escape leading/trailing spaces, in order to + // pretty-print does not change the content + serialize_as!(char_space: ' ' => " "); + + serialize_as!(str_non_escaped: "non-escaped string" => "non-escaped string"); + serialize_as!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + + err!(bytes: Bytes(b"<\"escaped & bytes'>") => Unsupported("`serialize_bytes` not supported yet")); + + serialize_as!(option_none: Option::<&str>::None => ""); + serialize_as!(option_some: Some("non-escaped string") => "non-escaped string"); + serialize_as!(option_some_empty: Some("") => ""); + + serialize_as!(unit: () => ""); + serialize_as!(unit_struct: Unit => ""); + serialize_as!(unit_struct_escaped: UnitEscaped => ""); + + serialize_as!(enum_unit: Enum::Unit => ""); + err!(enum_unit_escaped: Enum::UnitEscaped + => Unsupported("character `<` is not allowed at the start of an XML name `<\"&'>`")); + + serialize_as!(newtype: Newtype(42) => "42"); + serialize_as!(enum_newtype: Enum::Newtype(42) => "42"); + + serialize_as!(seq: vec![1, 2, 3] + => "1\n\ + 2\n\ + 3"); + serialize_as!(seq_empty: Vec::::new() => ""); + serialize_as!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) + => "<"&'>\n\ + with\t\r\n spaces\n\ + 3"); + serialize_as!(tuple_struct: Tuple("first", 42) + => "first\n\ + 42"); + serialize_as!(enum_tuple: Enum::Tuple("first", 42) + => "first\n\ + 42"); + + serialize_as!(map: BTreeMap::from([("_1", 2), ("_3", 4)]) + => "\n \ + <_1>2\n \ + <_3>4\n\ + "); + serialize_as!(struct_: Struct { key: "answer", val: (42, 42) } + => "\n \ + answer\n \ + 42\n \ + 42\n\ + "); + serialize_as!(enum_struct: Enum::Struct { key: "answer", val: (42, 42) } + => "\n \ + answer\n \ + 42\n \ + 42\n\ + "); + + /// Special field name `$text` should be serialized as text content. + /// Sequences serialized as an `xs:list` content + mod text { + use super::*; + + /// `$text` key in a map + mod map { + use super::*; + use pretty_assertions::assert_eq; + + macro_rules! text { + ($name:ident: $data:expr) => { + serialize_as!($name: + BTreeMap::from([("$text", $data)]) + => ""); + }; + ($name:ident: $data:expr => $expected:literal) => { + serialize_as!($name: + BTreeMap::from([("$text", $data)]) + => concat!("\n ", $expected,"\n")); + }; + } + + text!(false_: false => "false"); + text!(true_: true => "true"); + + text!(i8_: -42i8 => "-42"); + text!(i16_: -4200i16 => "-4200"); + text!(i32_: -42000000i32 => "-42000000"); + text!(i64_: -42000000000000i64 => "-42000000000000"); + text!(isize_: -42000000000000isize => "-42000000000000"); + + text!(u8_: 42u8 => "42"); + text!(u16_: 4200u16 => "4200"); + text!(u32_: 42000000u32 => "42000000"); + text!(u64_: 42000000000000u64 => "42000000000000"); + text!(usize_: 42000000000000usize => "42000000000000"); + + serde_if_integer128! { + text!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); + text!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + } + + text!(f32_: 4.2f32 => "4.2"); + text!(f64_: 4.2f64 => "4.2"); + + text!(char_non_escaped: 'h' => "h"); + text!(char_lt: '<' => "<"); + text!(char_gt: '>' => ">"); + text!(char_amp: '&' => "&"); + text!(char_apos: '\'' => "'"); + text!(char_quot: '"' => """); + //TODO: add a setting to escape leading/trailing spaces, in order to + // pretty-print does not change the content + text!(char_space: ' ' => " "); + + text!(str_non_escaped: "non-escaped string" => "non-escaped string"); + text!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + + err!(bytes: + Text { + before: "answer", + content: Bytes(b"<\"escaped & bytes'>"), + after: "answer", + } + => Unsupported("`serialize_bytes` not supported yet")); + + text!(option_none: Option::<&str>::None); + text!(option_some: Some("non-escaped string") => "non-escaped string"); + text!(option_some_empty_str: Some("")); + + text!(unit: ()); + text!(unit_struct: Unit); + text!(unit_struct_escaped: UnitEscaped); + + text!(enum_unit: Enum::Unit => "Unit"); + text!(enum_unit_escaped: Enum::UnitEscaped => "<"&'>"); + + text!(newtype: Newtype(42) => "42"); + // We have no space where name of a variant can be stored + err!(enum_newtype: + Text { + before: "answer", + content: Enum::Newtype(42), + after: "answer", + } + => Unsupported("enum newtype variant `Enum::Newtype` cannot be serialized as an attribute or text content value")); + + // Sequences are serialized separated by spaces, all spaces inside are escaped + text!(seq: vec![1, 2, 3] => "1 2 3"); + text!(seq_empty: Vec::::new()); + text!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) + => "<"&'> \ + with spaces \ + 3"); + text!(tuple_struct: Tuple("first", 42) => "first 42"); + // We have no space where name of a variant can be stored + err!(enum_tuple: + Text { + before: "answer", + content: Enum::Tuple("first", 42), + after: "answer", + } + => Unsupported("enum tuple variant `Enum::Tuple` cannot be serialized as an attribute or text content value")); + + // Complex types cannot be serialized in `$text` field + err!(map: + Text { + before: "answer", + content: BTreeMap::from([("_1", 2), ("_3", 4)]), + after: "answer", + } + => Unsupported("map cannot be serialized as an attribute or text content value")); + err!(struct_: + Text { + before: "answer", + content: Struct { key: "answer", val: (42, 42) }, + after: "answer", + } + => Unsupported("struct `Struct` cannot be serialized as an attribute or text content value")); + err!(enum_struct: + Text { + before: "answer", + content: Enum::Struct { key: "answer", val: (42, 42) }, + after: "answer", + } + => Unsupported("enum struct variant `Enum::Struct` cannot be serialized as an attribute or text content value")); + } + + /// `$text` field inside a struct + mod struct_ { + use super::*; + use pretty_assertions::assert_eq; + + macro_rules! text { + ($name:ident: $data:expr) => { + serialize_as!($name: + Text { + before: "answer", + content: $data, + after: "answer", + } + => "\n \ + answer\n \ + answer\n\ + "); + }; + ($name:ident: $data:expr => $expected:literal) => { + serialize_as!($name: + Text { + before: "answer", + content: $data, + after: "answer", + } + => concat!( + "\n answer\n ", + $expected, + "\n answer\n", + )); + }; + } + + text!(false_: false => "false"); + text!(true_: true => "true"); + + text!(i8_: -42i8 => "-42"); + text!(i16_: -4200i16 => "-4200"); + text!(i32_: -42000000i32 => "-42000000"); + text!(i64_: -42000000000000i64 => "-42000000000000"); + text!(isize_: -42000000000000isize => "-42000000000000"); + + text!(u8_: 42u8 => "42"); + text!(u16_: 4200u16 => "4200"); + text!(u32_: 42000000u32 => "42000000"); + text!(u64_: 42000000000000u64 => "42000000000000"); + text!(usize_: 42000000000000usize => "42000000000000"); + + serde_if_integer128! { + text!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); + text!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + } + + text!(f32_: 4.2f32 => "4.2"); + text!(f64_: 4.2f64 => "4.2"); + + text!(char_non_escaped: 'h' => "h"); + text!(char_lt: '<' => "<"); + text!(char_gt: '>' => ">"); + text!(char_amp: '&' => "&"); + text!(char_apos: '\'' => "'"); + text!(char_quot: '"' => """); + //TODO: add a setting to escape leading/trailing spaces, in order to + // pretty-print does not change the content + text!(char_space: ' ' => " "); + + text!(str_non_escaped: "non-escaped string" => "non-escaped string"); + text!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + + err!(bytes: + Text { + before: "answer", + content: Bytes(b"<\"escaped & bytes'>"), + after: "answer", + } + => Unsupported("`serialize_bytes` not supported yet")); + + text!(option_none: Option::<&str>::None); + text!(option_some: Some("non-escaped string") => "non-escaped string"); + text!(option_some_empty_str: Some("")); + + text!(unit: ()); + text!(unit_struct: Unit); + text!(unit_struct_escaped: UnitEscaped); + + text!(enum_unit: Enum::Unit => "Unit"); + text!(enum_unit_escaped: Enum::UnitEscaped => "<"&'>"); + + text!(newtype: Newtype(42) => "42"); + // We have no space where name of a variant can be stored + err!(enum_newtype: + Text { + before: "answer", + content: Enum::Newtype(42), + after: "answer", + } + => Unsupported("enum newtype variant `Enum::Newtype` cannot be serialized as an attribute or text content value")); + + // Sequences are serialized separated by spaces, all spaces inside are escaped + text!(seq: vec![1, 2, 3] => "1 2 3"); + text!(seq_empty: Vec::::new()); + text!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) + => "<"&'> \ + with spaces \ + 3"); + text!(tuple_struct: Tuple("first", 42) => "first 42"); + // We have no space where name of a variant can be stored + err!(enum_tuple: + Text { + before: "answer", + content: Enum::Tuple("first", 42), + after: "answer", + } + => Unsupported("enum tuple variant `Enum::Tuple` cannot be serialized as an attribute or text content value")); + + // Complex types cannot be serialized in `$text` field + err!(map: + Text { + before: "answer", + content: BTreeMap::from([("_1", 2), ("_3", 4)]), + after: "answer", + } + => Unsupported("map cannot be serialized as an attribute or text content value")); + err!(struct_: + Text { + before: "answer", + content: Struct { key: "answer", val: (42, 42) }, + after: "answer", + } + => Unsupported("struct `Struct` cannot be serialized as an attribute or text content value")); + err!(enum_struct: + Text { + before: "answer", + content: Enum::Struct { key: "answer", val: (42, 42) }, + after: "answer", + } + => Unsupported("enum struct variant `Enum::Struct` cannot be serialized as an attribute or text content value")); + } + + /// `$text` field inside a struct variant of an enum + mod enum_struct { + use super::*; + use pretty_assertions::assert_eq; + + macro_rules! text { + ($name:ident: $data:expr) => { + serialize_as!($name: + SpecialEnum::Text { + before: "answer", + content: $data, + after: "answer", + } + => "\n \ + answer\n \ + answer\n\ + "); + }; + ($name:ident: $data:expr => $expected:literal) => { + serialize_as!($name: + SpecialEnum::Text { + before: "answer", + content: $data, + after: "answer", + } + => concat!( + "\n answer\n ", + $expected, + "\n answer\n", + )); + }; + } + + text!(false_: false => "false"); + text!(true_: true => "true"); + + text!(i8_: -42i8 => "-42"); + text!(i16_: -4200i16 => "-4200"); + text!(i32_: -42000000i32 => "-42000000"); + text!(i64_: -42000000000000i64 => "-42000000000000"); + text!(isize_: -42000000000000isize => "-42000000000000"); + + text!(u8_: 42u8 => "42"); + text!(u16_: 4200u16 => "4200"); + text!(u32_: 42000000u32 => "42000000"); + text!(u64_: 42000000000000u64 => "42000000000000"); + text!(usize_: 42000000000000usize => "42000000000000"); + + serde_if_integer128! { + text!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); + text!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + } + + text!(f32_: 4.2f32 => "4.2"); + text!(f64_: 4.2f64 => "4.2"); + + text!(char_non_escaped: 'h' => "h"); + text!(char_lt: '<' => "<"); + text!(char_gt: '>' => ">"); + text!(char_amp: '&' => "&"); + text!(char_apos: '\'' => "'"); + text!(char_quot: '"' => """); + //TODO: add a setting to escape leading/trailing spaces, in order to + // pretty-print does not change the content + text!(char_space: ' ' => " "); + + text!(str_non_escaped: "non-escaped string" => "non-escaped string"); + text!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + + err!(bytes: + SpecialEnum::Text { + before: "answer", + content: Bytes(b"<\"escaped & bytes'>"), + after: "answer", + } + => Unsupported("`serialize_bytes` not supported yet")); + + text!(option_none: Option::<&str>::None); + text!(option_some: Some("non-escaped string") => "non-escaped string"); + text!(option_some_empty_str: Some("")); + + text!(unit: ()); + text!(unit_struct: Unit); + text!(unit_struct_escaped: UnitEscaped); + + text!(enum_unit: Enum::Unit => "Unit"); + text!(enum_unit_escaped: Enum::UnitEscaped => "<"&'>"); + + text!(newtype: Newtype(42) => "42"); + // We have no space where name of a variant can be stored + err!(enum_newtype: + SpecialEnum::Text { + before: "answer", + content: Enum::Newtype(42), + after: "answer", + } + => Unsupported("enum newtype variant `Enum::Newtype` cannot be serialized as an attribute or text content value")); + + // Sequences are serialized separated by spaces, all spaces inside are escaped + text!(seq: vec![1, 2, 3] => "1 2 3"); + text!(seq_empty: Vec::::new()); + text!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) + => "<"&'> \ + with spaces \ + 3"); + text!(tuple_struct: Tuple("first", 42) => "first 42"); + // We have no space where name of a variant can be stored + err!(enum_tuple: + SpecialEnum::Text { + before: "answer", + content: Enum::Tuple("first", 42), + after: "answer", + } + => Unsupported("enum tuple variant `Enum::Tuple` cannot be serialized as an attribute or text content value")); + + // Complex types cannot be serialized in `$text` field + err!(map: + SpecialEnum::Text { + before: "answer", + content: BTreeMap::from([("_1", 2), ("_3", 4)]), + after: "answer", + } + => Unsupported("map cannot be serialized as an attribute or text content value")); + err!(struct_: + SpecialEnum::Text { + before: "answer", + content: Struct { key: "answer", val: (42, 42) }, + after: "answer", + } + => Unsupported("struct `Struct` cannot be serialized as an attribute or text content value")); + err!(enum_struct: + SpecialEnum::Text { + before: "answer", + content: Enum::Struct { key: "answer", val: (42, 42) }, + after: "answer", + } + => Unsupported("enum struct variant `Enum::Struct` cannot be serialized as an attribute or text content value")); + } + } + + /// Special field name `$value` should be serialized using name, provided + /// by the type of value instead of a key. Sequences serialized as a list + /// of tags with that name (each element can have their own name) + mod value { + use super::*; + + /// `$value` key in a map + mod map { + use super::*; + use pretty_assertions::assert_eq; + + macro_rules! value { + ($name:ident: $data:expr) => { + serialize_as!($name: + BTreeMap::from([("$value", $data)]) + => ""); + }; + ($name:ident: $data:expr => $expected:literal) => { + serialize_as!($name: + BTreeMap::from([("$value", $data)]) + => concat!("\n ", $expected,"\n")); + }; + } + + value!(false_: false => "false"); + value!(true_: true => "true"); + + value!(i8_: -42i8 => "-42"); + value!(i16_: -4200i16 => "-4200"); + value!(i32_: -42000000i32 => "-42000000"); + value!(i64_: -42000000000000i64 => "-42000000000000"); + value!(isize_: -42000000000000isize => "-42000000000000"); + + value!(u8_: 42u8 => "42"); + value!(u16_: 4200u16 => "4200"); + value!(u32_: 42000000u32 => "42000000"); + value!(u64_: 42000000000000u64 => "42000000000000"); + value!(usize_: 42000000000000usize => "42000000000000"); + + serde_if_integer128! { + value!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); + value!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + } + + value!(f32_: 4.2f32 => "4.2"); + value!(f64_: 4.2f64 => "4.2"); + + value!(char_non_escaped: 'h' => "h"); + value!(char_lt: '<' => "<"); + value!(char_gt: '>' => ">"); + value!(char_amp: '&' => "&"); + value!(char_apos: '\'' => "'"); + value!(char_quot: '"' => """); + //TODO: add a setting to escape leading/trailing spaces, in order to + // pretty-print does not change the content + value!(char_space: ' ' => " "); + + value!(str_non_escaped: "non-escaped string" => "non-escaped string"); + value!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + + err!(bytes: + BTreeMap::from([("$value", Bytes(b"<\"escaped & bytes'>"))]) + => Unsupported("`serialize_bytes` not supported yet")); + + value!(option_none: Option::<&str>::None); + value!(option_some: Some("non-escaped string") => "non-escaped string"); + value!(option_some_empty_str: Some("")); + + value!(unit: ()); + value!(unit_struct: Unit); + value!(unit_struct_escaped: UnitEscaped); + + value!(enum_unit: Enum::Unit => ""); + err!(enum_unit_escaped: + BTreeMap::from([("$value", Enum::UnitEscaped)]) + => Unsupported("character `<` is not allowed at the start of an XML name `<\"&'>`")); + + value!(newtype: Newtype(42) => "42"); + value!(enum_newtype: Enum::Newtype(42) => "42"); + + value!(seq: vec![1, 2, 3] => "1\n 2\n 3"); + value!(seq_empty: Vec::::new()); + value!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) + => "<"&'>\n \ + with\t\r\n spaces\n \ + 3"); + value!(tuple_struct: Tuple("first", 42) => "first\n 42"); + value!(enum_tuple: Enum::Tuple("first", 42) + => "first\n \ + 42"); + + // We cannot wrap map or struct in any container and should not + // flatten it, so it is impossible to serialize maps and structs + err!(map: + BTreeMap::from([("$value", BTreeMap::from([("_1", 2), ("_3", 4)]))]) + => Unsupported("serialization of map types is not supported in `$value` field")); + err!(struct_: + BTreeMap::from([("$value", Struct { key: "answer", val: (42, 42) })]) + => Unsupported("serialization of struct `Struct` is not supported in `$value` field")); + value!(enum_struct: + Enum::Struct { key: "answer", val: (42, 42) } + => "\n \ + answer\n \ + 42\n \ + 42\n \ + "); + } + + /// `$value` field inside a struct + mod struct_ { + use super::*; + use pretty_assertions::assert_eq; + + macro_rules! value { + ($name:ident: $data:expr) => { + serialize_as!($name: + Value { + before: "answer", + content: $data, + after: "answer", + } + => "\n \ + answer\n \ + answer\n\ + "); + }; + ($name:ident: $data:expr => $expected:literal) => { + serialize_as!($name: + Value { + before: "answer", + content: $data, + after: "answer", + } + => concat!( + "\n answer\n ", + $expected, + "\n answer\n", + )); + }; + } + + value!(false_: false => "false"); + value!(true_: true => "true"); + + value!(i8_: -42i8 => "-42"); + value!(i16_: -4200i16 => "-4200"); + value!(i32_: -42000000i32 => "-42000000"); + value!(i64_: -42000000000000i64 => "-42000000000000"); + value!(isize_: -42000000000000isize => "-42000000000000"); + + value!(u8_: 42u8 => "42"); + value!(u16_: 4200u16 => "4200"); + value!(u32_: 42000000u32 => "42000000"); + value!(u64_: 42000000000000u64 => "42000000000000"); + value!(usize_: 42000000000000usize => "42000000000000"); + + serde_if_integer128! { + value!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); + value!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + } + + value!(f32_: 4.2f32 => "4.2"); + value!(f64_: 4.2f64 => "4.2"); + + value!(char_non_escaped: 'h' => "h"); + value!(char_lt: '<' => "<"); + value!(char_gt: '>' => ">"); + value!(char_amp: '&' => "&"); + value!(char_apos: '\'' => "'"); + value!(char_quot: '"' => """); + //TODO: add a setting to escape leading/trailing spaces, in order to + // pretty-print does not change the content + value!(char_space: ' ' => " "); + + value!(str_non_escaped: "non-escaped string" => "non-escaped string"); + value!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + + err!(bytes: + Value { + before: "answer", + content: Bytes(b"<\"escaped & bytes'>"), + after: "answer", + } + => Unsupported("`serialize_bytes` not supported yet")); + + value!(option_none: Option::<&str>::None); + value!(option_some: Some("non-escaped string") => "non-escaped string"); + value!(option_some_empty_str: Some("")); + + value!(unit: ()); + value!(unit_struct: Unit); + value!(unit_struct_escaped: UnitEscaped); + + value!(enum_unit: Enum::Unit => ""); + err!(enum_unit_escaped: + Value { + before: "answer", + content: Enum::UnitEscaped, + after: "answer", + } + => Unsupported("character `<` is not allowed at the start of an XML name `<\"&'>`")); + + value!(newtype: Newtype(42) => "42"); + value!(enum_newtype: Enum::Newtype(42) => "42"); + + // Note that sequences of primitives serialized without delimiters! + value!(seq: vec![1, 2, 3] => "1\n 2\n 3"); + value!(seq_empty: Vec::::new()); + value!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) + => "<"&'>\n \ + with\t\r\n spaces\n \ + 3"); + value!(tuple_struct: Tuple("first", 42) => "first\n 42"); + value!(enum_tuple: Enum::Tuple("first", 42) + => "first\n \ + 42"); + + // We cannot wrap map or struct in any container and should not + // flatten it, so it is impossible to serialize maps and structs + err!(map: + Value { + before: "answer", + content: BTreeMap::from([("_1", 2), ("_3", 4)]), + after: "answer", + } + => Unsupported("serialization of map types is not supported in `$value` field")); + err!(struct_: + Value { + before: "answer", + content: Struct { key: "answer", val: (42, 42) }, + after: "answer", + } + => Unsupported("serialization of struct `Struct` is not supported in `$value` field")); + value!(enum_struct: + Enum::Struct { key: "answer", val: (42, 42) } + => "\n \ + answer\n \ + 42\n \ + 42\n \ + "); + } + + /// `$value` field inside a struct variant of an enum + mod enum_struct { + use super::*; + use pretty_assertions::assert_eq; + + macro_rules! value { + ($name:ident: $data:expr) => { + serialize_as!($name: + SpecialEnum::Value { + before: "answer", + content: $data, + after: "answer", + } + => "\n \ + answer\n \ + answer\n\ + "); + }; + ($name:ident: $data:expr => $expected:literal) => { + serialize_as!($name: + SpecialEnum::Value { + before: "answer", + content: $data, + after: "answer", + } + => concat!( + "\n answer\n ", + $expected, + "\n answer\n", + )); + }; + } + + value!(false_: false => "false"); + value!(true_: true => "true"); + + value!(i8_: -42i8 => "-42"); + value!(i16_: -4200i16 => "-4200"); + value!(i32_: -42000000i32 => "-42000000"); + value!(i64_: -42000000000000i64 => "-42000000000000"); + value!(isize_: -42000000000000isize => "-42000000000000"); + + value!(u8_: 42u8 => "42"); + value!(u16_: 4200u16 => "4200"); + value!(u32_: 42000000u32 => "42000000"); + value!(u64_: 42000000000000u64 => "42000000000000"); + value!(usize_: 42000000000000usize => "42000000000000"); + + serde_if_integer128! { + value!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); + value!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + } + + value!(f32_: 4.2f32 => "4.2"); + value!(f64_: 4.2f64 => "4.2"); + + value!(char_non_escaped: 'h' => "h"); + value!(char_lt: '<' => "<"); + value!(char_gt: '>' => ">"); + value!(char_amp: '&' => "&"); + value!(char_apos: '\'' => "'"); + value!(char_quot: '"' => """); + //TODO: add a setting to escape leading/trailing spaces, in order to + // pretty-print does not change the content + value!(char_space: ' ' => " "); + + value!(str_non_escaped: "non-escaped string" => "non-escaped string"); + value!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + + err!(bytes: + SpecialEnum::Value { + before: "answer", + content: Bytes(b"<\"escaped & bytes'>"), + after: "answer", + } + => Unsupported("`serialize_bytes` not supported yet")); + + value!(option_none: Option::<&str>::None); + value!(option_some: Some("non-escaped string") => "non-escaped string"); + value!(option_some_empty_str: Some("")); + + value!(unit: ()); + value!(unit_struct: Unit); + value!(unit_struct_escaped: UnitEscaped); + + value!(enum_unit: Enum::Unit => ""); + err!(enum_unit_escaped: + SpecialEnum::Value { + before: "answer", + content: Enum::UnitEscaped, + after: "answer", + } + => Unsupported("character `<` is not allowed at the start of an XML name `<\"&'>`")); + + value!(newtype: Newtype(42) => "42"); + value!(enum_newtype: Enum::Newtype(42) => "42"); + + // Note that sequences of primitives serialized without delimiters! + value!(seq: vec![1, 2, 3] => "1\n 2\n 3"); + value!(seq_empty: Vec::::new()); + value!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) + => "<"&'>\n \ + with\t\r\n spaces\n \ + 3"); + value!(tuple_struct: Tuple("first", 42) => "first\n 42"); + value!(enum_tuple: Enum::Tuple("first", 42) + => "first\n \ + 42"); + + // We cannot wrap map or struct in any container and should not + // flatten it, so it is impossible to serialize maps and structs + err!(map: + SpecialEnum::Value { + before: "answer", + content: BTreeMap::from([("_1", 2), ("_3", 4)]), + after: "answer", + } + => Unsupported("serialization of map types is not supported in `$value` field")); + err!(struct_: + SpecialEnum::Value { + before: "answer", + content: Struct { key: "answer", val: (42, 42) }, + after: "answer", + } + => Unsupported("serialization of struct `Struct` is not supported in `$value` field")); + value!(enum_struct: + Enum::Struct { key: "answer", val: (42, 42) } + => "\n \ + answer\n \ + 42\n \ + 42\n \ + "); + } + } + + mod attributes { + use super::*; + use pretty_assertions::assert_eq; + + serialize_as!(map_attr: BTreeMap::from([("@key1", 1), ("@key2", 2)]) + => r#""#); + serialize_as!(map_mixed: BTreeMap::from([("@key1", 1), ("key2", 2)]) + => "\n \ + 2\n\ + "); + + serialize_as!(struct_: Attributes { key: "answer", val: (42, 42) } + => r#""#); + serialize_as!(struct_before: AttributesBefore { key: "answer", val: 42 } + => "\n \ + 42\n\ + "); + serialize_as!(struct_after: AttributesAfter { key: "answer", val: 42 } + => "\n \ + answer\n\ + "); + + serialize_as!(enum_: Enum::Attributes { key: "answer", val: (42, 42) } + => r#""#); + serialize_as!(enum_before: Enum::AttributesBefore { key: "answer", val: 42 } + => "\n \ + 42\n\ + "); + serialize_as!(enum_after: Enum::AttributesAfter { key: "answer", val: 42 } + => "\n \ + answer\n\ + "); + + /// Test for https://github.com/tafia/quick-xml/issues/252 + mod optional { + use super::*; + use pretty_assertions::assert_eq; + + serialize_as!(none: + OptionalAttributes { a: None, b: None } + => r#""#); + serialize_as!(some_empty_str: + OptionalAttributes { + a: Some(""), + b: Some("") + } + => r#""#); + serialize_as!(some_non_empty: + OptionalAttributes { + a: Some("a"), + b: Some("b") + } + => r#""#); + } + } + + /// Test for https://github.com/tafia/quick-xml/issues/252 + mod optional { + use super::*; + use pretty_assertions::assert_eq; + + serialize_as!(none: + OptionalElements { a: None, b: None } + => "\n \ + \n\ + "); + serialize_as!(some_empty_str: + OptionalElements { + a: Some(""), + b: Some("") + } + => "\n \ + \n \ + \n\ + "); + serialize_as!(some_non_empty: + OptionalElements { + a: Some("a"), + b: Some("b") + } + => "\n \ + a\n \ + b\n\ + "); + } + } } diff --git a/src/se/mod.rs b/src/se/mod.rs index 130f23f3..e981e071 100644 --- a/src/se/mod.rs +++ b/src/se/mod.rs @@ -84,11 +84,12 @@ use crate::{ de::PRIMITIVE_PREFIX, errors::serialize::DeError, events::{BytesEnd, BytesStart, BytesText, Event}, - writer::Writer, + writer::{Indentation, Writer}, }; use serde::ser::{self, Serialize}; use serde::serde_if_integer128; use std::io::Write; +use std::str::from_utf8; /// Serialize struct into a `Write`r pub fn to_writer(writer: W, value: &S) -> Result<(), DeError> { @@ -221,6 +222,55 @@ impl<'n> XmlName<'n> { //////////////////////////////////////////////////////////////////////////////////////////////////// +pub(crate) enum Indent<'i> { + None, + Owned(Indentation), + Borrow(&'i mut Indentation), +} + +impl<'i> Indent<'i> { + pub fn borrow(&mut self) -> Indent { + match self { + Self::None => Indent::None, + Self::Owned(ref mut i) => Indent::Borrow(i), + Self::Borrow(i) => Indent::Borrow(i), + } + } + + pub fn increase(&mut self) { + match self { + Self::None => {} + Self::Owned(i) => i.grow(), + Self::Borrow(i) => i.grow(), + } + } + + pub fn decrease(&mut self) { + match self { + Self::None => {} + Self::Owned(i) => i.shrink(), + Self::Borrow(i) => i.shrink(), + } + } + + pub fn write_indent(&mut self, mut writer: W) -> Result<(), DeError> { + match self { + Self::None => {} + Self::Owned(i) => { + writer.write_char('\n')?; + writer.write_str(from_utf8(i.current())?)?; + } + Self::Borrow(i) => { + writer.write_char('\n')?; + writer.write_str(from_utf8(i.current())?)?; + } + } + Ok(()) + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// + /// A Serializer pub struct Serializer<'r, W: Write> { writer: Writer, diff --git a/src/se/simple_type.rs b/src/se/simple_type.rs index 14f0160b..17f6dc0b 100644 --- a/src/se/simple_type.rs +++ b/src/se/simple_type.rs @@ -5,7 +5,7 @@ use crate::errors::serialize::DeError; use crate::escapei::_escape; -use crate::se::QuoteLevel; +use crate::se::{Indent, QuoteLevel}; use serde::ser::{ Impossible, Serialize, SerializeSeq, SerializeTuple, SerializeTupleStruct, Serializer, }; @@ -327,30 +327,33 @@ impl Serializer for AtomicSerializer { /// - CDATA content (`<...>`) /// /// [simple types]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition -pub struct SimpleTypeSerializer { +pub struct SimpleTypeSerializer<'i, W: Write> { /// Writer to which this serializer writes content pub writer: W, /// Target for which element is serializing. Affects additional characters to escape. pub target: QuoteTarget, /// Defines which XML characters need to be escaped pub level: QuoteLevel, + /// Indent that should be written before the content if content is not an empty string + pub(crate) indent: Indent<'i>, } -impl SimpleTypeSerializer { +impl<'i, W: Write> SimpleTypeSerializer<'i, W> { fn write_str(&mut self, value: &str) -> Result<(), DeError> { + self.indent.write_indent(&mut self.writer)?; Ok(self .writer .write_str(&escape_list(value, self.target, self.level))?) } } -impl Serializer for SimpleTypeSerializer { +impl<'i, W: Write> Serializer for SimpleTypeSerializer<'i, W> { type Ok = W; type Error = DeError; - type SerializeSeq = SimpleSeq; - type SerializeTuple = SimpleSeq; - type SerializeTupleStruct = SimpleSeq; + type SerializeSeq = SimpleSeq<'i, W>; + type SerializeTuple = SimpleSeq<'i, W>; + type SerializeTupleStruct = SimpleSeq<'i, W>; type SerializeTupleVariant = Impossible; type SerializeMap = Impossible; type SerializeStruct = Impossible; @@ -359,6 +362,9 @@ impl Serializer for SimpleTypeSerializer { write_primitive!(); fn serialize_str(mut self, value: &str) -> Result { + if value.is_empty() { + self.indent = Indent::None; + } self.write_str(value)?; Ok(self.writer) } @@ -394,6 +400,7 @@ impl Serializer for SimpleTypeSerializer { target: self.target, level: self.level, first: true, + indent: self.indent, }) } @@ -457,15 +464,17 @@ impl Serializer for SimpleTypeSerializer { } /// Serializer for a sequence of atomic values delimited by space -pub struct SimpleSeq { +pub struct SimpleSeq<'i, W: Write> { writer: W, target: QuoteTarget, level: QuoteLevel, /// If `true`, nothing was written yet first: bool, + /// Indent that should be written before the content if content is not an empty string + indent: Indent<'i>, } -impl SerializeSeq for SimpleSeq { +impl<'i, W: Write> SerializeSeq for SimpleSeq<'i, W> { type Ok = W; type Error = DeError; @@ -473,7 +482,11 @@ impl SerializeSeq for SimpleSeq { where T: ?Sized + Serialize, { - if !self.first { + // Write indent for the first element and delimiter for others + //FIXME: sequence with only empty strings will be serialized as indent only + delimiters + if self.first { + self.indent.write_indent(&mut self.writer)?; + } else { self.writer.write_char(' ')?; } self.first = false; @@ -491,7 +504,7 @@ impl SerializeSeq for SimpleSeq { } } -impl SerializeTuple for SimpleSeq { +impl<'i, W: Write> SerializeTuple for SimpleSeq<'i, W> { type Ok = W; type Error = DeError; @@ -509,7 +522,7 @@ impl SerializeTuple for SimpleSeq { } } -impl SerializeTupleStruct for SimpleSeq { +impl<'i, W: Write> SerializeTupleStruct for SimpleSeq<'i, W> { type Ok = W; type Error = DeError; @@ -940,6 +953,7 @@ mod tests { writer: String::new(), target: QuoteTarget::Text, level: QuoteLevel::Full, + indent: Indent::None, }; let buffer = $data.serialize(ser).unwrap(); @@ -959,6 +973,7 @@ mod tests { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, + indent: Indent::None, }; match $data.serialize(ser).unwrap_err() { diff --git a/src/writer.rs b/src/writer.rs index bf12d83c..2c633c74 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -171,9 +171,7 @@ impl Writer { if let Some(ref i) = self.indent { if i.should_line_break { self.writer.write_all(b"\n").map_err(Error::Io)?; - self.writer - .write_all(&i.indents[..i.indents_len]) - .map_err(Error::Io)?; + self.writer.write_all(i.current()).map_err(Error::Io)?; } } self.write(before)?; @@ -196,9 +194,7 @@ impl Writer { pub fn write_indent(&mut self) -> Result<()> { if let Some(ref i) = self.indent { self.writer.write_all(b"\n").map_err(Error::Io)?; - self.writer - .write_all(&i.indents[..i.indents_len]) - .map_err(Error::Io)?; + self.writer.write_all(i.current()).map_err(Error::Io)?; } Ok(()) } @@ -337,7 +333,7 @@ impl<'a, W: Write> ElementWriter<'a, W> { } #[derive(Clone)] -struct Indentation { +pub(crate) struct Indentation { should_line_break: bool, indent_char: u8, indent_size: usize, @@ -346,8 +342,8 @@ struct Indentation { } impl Indentation { - fn new(indent_char: u8, indent_size: usize) -> Indentation { - Indentation { + pub fn new(indent_char: u8, indent_size: usize) -> Self { + Self { should_line_break: false, indent_char, indent_size, @@ -356,16 +352,23 @@ impl Indentation { } } - fn grow(&mut self) { + /// Increase indentation by one level + pub fn grow(&mut self) { self.indents_len += self.indent_size; if self.indents_len > self.indents.len() { self.indents.resize(self.indents_len, self.indent_char); } } - fn shrink(&mut self) { + /// Decrease indentation by one level. Do nothing, if level already zero + pub fn shrink(&mut self) { self.indents_len = self.indents_len.saturating_sub(self.indent_size); } + + /// Returns indent string for current level + pub fn current(&self) -> &[u8] { + &self.indents[..self.indents_len] + } } #[cfg(test)]