diff --git a/Changelog.md b/Changelog.md index 0da34663..a0bfeeeb 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,6 +17,9 @@ ### Bug Fixes +- [#655]: Do not write indent before and after `$text` fields and those `$value` fields + that are serialized as a text (for example, `usize` or `String`). + ### Misc Changes - [#227]: Split `SeError` from `DeError` in the `serialize` feature. @@ -26,10 +29,13 @@ - [#811]: Renamed `Error::EscapeError` to `Error::Escape` to match other variants. - [#811]: Narrow down error return type from `Error` where only one variant is ever returned: attribute related methods on `BytesStart` and `BytesDecl` returns `AttrError` +- [#820]: Classify output of the `Serializer` by returning an enumeration with kind of written data [#227]: https://github.com/tafia/quick-xml/issues/227 +[#655]: https://github.com/tafia/quick-xml/issues/655 [#810]: https://github.com/tafia/quick-xml/pull/810 [#811]: https://github.com/tafia/quick-xml/pull/811 +[#820]: https://github.com/tafia/quick-xml/pull/820 ## 0.36.2 -- 2024-09-20 diff --git a/src/de/simple_type.rs b/src/de/simple_type.rs index 5c102674..d78db8fc 100644 --- a/src/de/simple_type.rs +++ b/src/de/simple_type.rs @@ -791,7 +791,7 @@ impl<'de, 'a> EnumAccess<'de> for SimpleTypeDeserializer<'de, 'a> { mod tests { use super::*; use crate::se::simple_type::{QuoteTarget, SimpleTypeSerializer}; - use crate::se::{Indent, QuoteLevel}; + use crate::se::QuoteLevel; use crate::utils::{ByteBuf, Bytes}; use serde::de::IgnoredAny; use serde::{Deserialize, Serialize}; @@ -828,7 +828,6 @@ mod tests { writer: String::new(), target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Indent::None, }) .unwrap(), xml @@ -943,7 +942,7 @@ mod tests { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Some(Indent::None), + write_delimiter: false, }) .unwrap(); assert_eq!(buffer, $input); diff --git a/src/se/content.rs b/src/se/content.rs index 50f3b92d..c07a2d9d 100644 --- a/src/se/content.rs +++ b/src/se/content.rs @@ -3,7 +3,7 @@ use crate::de::TEXT_KEY; use crate::se::element::{ElementSerializer, Struct, Tuple}; use crate::se::simple_type::{QuoteTarget, SimpleTypeSerializer}; -use crate::se::{Indent, QuoteLevel, SeError, XmlName}; +use crate::se::{Indent, QuoteLevel, SeError, WriteResult, XmlName}; use serde::ser::{ Impossible, Serialize, SerializeSeq, SerializeTuple, SerializeTupleStruct, Serializer, }; @@ -15,7 +15,7 @@ macro_rules! write_primitive { #[inline] fn $method(self, value: $ty) -> Result { self.into_simple_type_serializer().$method(value)?; - Ok(()) + Ok(WriteResult::Text) } }; } @@ -24,7 +24,9 @@ macro_rules! write_primitive { /// A serializer used to serialize content of an element. It does not write /// surrounding tags. Unlike the [`ElementSerializer`], this serializer serializes -/// enums using variant names as tag names, i. e. as `...` +/// enums using variant names as tag names, i. e. as `...`. +/// +/// Returns the classification of the last written type. /// /// This serializer does the following: /// - numbers converted to a decimal representation and serialized as naked strings; @@ -67,7 +69,7 @@ pub struct ContentSerializer<'w, 'i, W: Write> { /// 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. + /// but only if content is not empty. This flag is reset after writing indent. pub write_indent: bool, // If `true`, then empty elements will be serialized as `` // instead of ``. @@ -78,17 +80,12 @@ pub struct ContentSerializer<'w, 'i, W: Write> { impl<'w, 'i, W: Write> ContentSerializer<'w, 'i, W> { /// Turns this serializer into serializer of a text content #[inline] - pub fn into_simple_type_serializer(self) -> SimpleTypeSerializer<'i, &'w mut W> { + pub fn into_simple_type_serializer(self) -> SimpleTypeSerializer<&'w mut 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 - }, } } @@ -107,7 +104,7 @@ impl<'w, 'i, W: Write> ContentSerializer<'w, 'i, W> { /// Writes `name` as self-closed tag #[inline] - pub(super) fn write_empty(mut self, name: XmlName) -> Result<(), SeError> { + pub(super) fn write_empty(mut self, name: XmlName) -> Result { self.write_indent()?; if self.expand_empty_elements { self.writer.write_char('<')?; @@ -120,13 +117,17 @@ impl<'w, 'i, W: Write> ContentSerializer<'w, 'i, W> { self.writer.write_str(name.0)?; self.writer.write_str("/>")?; } - Ok(()) + Ok(WriteResult::Element) } /// Writes simple type content between `name` tags - pub(super) fn write_wrapped(mut self, name: XmlName, serialize: S) -> Result<(), SeError> + pub(super) fn write_wrapped( + mut self, + name: XmlName, + serialize: S, + ) -> Result where - S: for<'a> FnOnce(SimpleTypeSerializer<'i, &'a mut W>) -> Result<&'a mut W, SeError>, + S: for<'a> FnOnce(SimpleTypeSerializer<&'a mut W>) -> Result<&'a mut W, SeError>, { self.write_indent()?; self.writer.write_char('<')?; @@ -138,7 +139,7 @@ impl<'w, 'i, W: Write> ContentSerializer<'w, 'i, W> { writer.write_str("')?; - Ok(()) + Ok(WriteResult::Element) } pub(super) fn write_indent(&mut self) -> Result<(), SeError> { @@ -151,12 +152,12 @@ impl<'w, 'i, W: Write> ContentSerializer<'w, 'i, W> { } impl<'w, 'i, W: Write> Serializer for ContentSerializer<'w, 'i, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; - type SerializeSeq = Self; - type SerializeTuple = Self; - type SerializeTupleStruct = Self; + type SerializeSeq = Seq<'w, 'i, W>; + type SerializeTuple = Seq<'w, 'i, W>; + type SerializeTupleStruct = Seq<'w, 'i, W>; type SerializeTupleVariant = Tuple<'w, 'i, W>; type SerializeMap = Impossible; type SerializeStruct = Impossible; @@ -182,21 +183,29 @@ impl<'w, 'i, W: Write> Serializer for ContentSerializer<'w, 'i, W> { write_primitive!(serialize_f32(f32)); write_primitive!(serialize_f64(f64)); - write_primitive!(serialize_char(char)); write_primitive!(serialize_bytes(&[u8])); + #[inline] + fn serialize_char(self, value: char) -> Result { + self.into_simple_type_serializer().serialize_char(value)?; + Ok(WriteResult::SensitiveText) + } + #[inline] fn serialize_str(self, value: &str) -> Result { if !value.is_empty() { self.into_simple_type_serializer().serialize_str(value)?; } - Ok(()) + Ok(WriteResult::SensitiveText) } /// Does not write anything #[inline] fn serialize_none(self) -> Result { - Ok(()) + // Classify `None` as sensitive to whitespaces, because this can be `Option`. + // Unfortunately, we do not known what the type the option contains, so have no chance + // to adapt our behavior to it. The safe variant is assume sensitiviness + Ok(WriteResult::SensitiveNothing) } fn serialize_some(self, value: &T) -> Result { @@ -206,13 +215,13 @@ impl<'w, 'i, W: Write> Serializer for ContentSerializer<'w, 'i, W> { /// Does not write anything #[inline] fn serialize_unit(self) -> Result { - Ok(()) + Ok(WriteResult::Nothing) } /// Does not write anything #[inline] fn serialize_unit_struct(self, _name: &'static str) -> Result { - Ok(()) + Ok(WriteResult::Nothing) } /// If `variant` is a special `$text` variant, then do nothing, otherwise @@ -224,7 +233,7 @@ impl<'w, 'i, W: Write> Serializer for ContentSerializer<'w, 'i, W> { variant: &'static str, ) -> Result { if variant == TEXT_KEY { - Ok(()) + Ok(WriteResult::Nothing) } else { let name = XmlName::try_from(variant)?; self.write_empty(name) @@ -251,18 +260,24 @@ impl<'w, 'i, W: Write> Serializer for ContentSerializer<'w, 'i, W> { ) -> Result { if variant == TEXT_KEY { value.serialize(self.into_simple_type_serializer())?; - Ok(()) + Ok(WriteResult::SensitiveText) } else { value.serialize(ElementSerializer { key: XmlName::try_from(variant)?, ser: self, - }) + })?; + Ok(WriteResult::Element) } } #[inline] fn serialize_seq(self, _len: Option) -> Result { - Ok(self) + Ok(Seq { + ser: self, + // If sequence if empty, nothing will be serialized. Because sequence can be of `Option`s + // we need to assume that writing indent may change the data and do not write anything + last: WriteResult::SensitiveNothing, + }) } #[inline] @@ -357,28 +372,38 @@ impl<'w, 'i, W: Write> Serializer for ContentSerializer<'w, 'i, W> { } } -impl<'w, 'i, W: Write> SerializeSeq for ContentSerializer<'w, 'i, W> { - type Ok = (); +//////////////////////////////////////////////////////////////////////////////////////////////////// + +/// Helper struct which remembers the classification of the last serialized element +/// and reports it when the sequence ends +pub struct Seq<'w, 'k, W: Write> { + ser: ContentSerializer<'w, 'k, W>, + /// Classification of the result of the last serialized element. + last: WriteResult, +} + +impl<'w, 'i, W: Write> SerializeSeq for Seq<'w, 'i, W> { + type Ok = WriteResult; type Error = SeError; fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> where T: ?Sized + Serialize, { - value.serialize(self.new_seq_element_serializer())?; - // Write indent for next element - self.write_indent = true; + self.last = value.serialize(self.ser.new_seq_element_serializer())?; + // Write indent for next element if indents are used + self.ser.write_indent = self.last.allow_indent(); Ok(()) } #[inline] fn end(self) -> Result { - Ok(()) + Ok(self.last) } } -impl<'w, 'i, W: Write> SerializeTuple for ContentSerializer<'w, 'i, W> { - type Ok = (); +impl<'w, 'i, W: Write> SerializeTuple for Seq<'w, 'i, W> { + type Ok = WriteResult; type Error = SeError; #[inline] @@ -395,8 +420,8 @@ impl<'w, 'i, W: Write> SerializeTuple for ContentSerializer<'w, 'i, W> { } } -impl<'w, 'i, W: Write> SerializeTupleStruct for ContentSerializer<'w, 'i, W> { - type Ok = (); +impl<'w, 'i, W: Write> SerializeTupleStruct for Seq<'w, 'i, W> { + type Ok = WriteResult; type Error = SeError; #[inline] @@ -422,6 +447,7 @@ pub(super) mod tests { use crate::utils::Bytes; use serde::Serialize; use std::collections::BTreeMap; + use WriteResult::*; #[derive(Debug, Serialize, PartialEq)] pub struct Unit; @@ -539,6 +565,9 @@ pub(super) mod tests { /// Checks that given `$data` successfully serialized as `$expected` macro_rules! serialize_as { ($name:ident: $data:expr => $expected:expr) => { + serialize_as!($name: $data => $expected, WriteResult::Element); + }; + ($name:ident: $data:expr => $expected:expr, $result:expr) => { #[test] fn $name() { let mut buffer = String::new(); @@ -550,8 +579,9 @@ pub(super) mod tests { expand_empty_elements: false, }; - $data.serialize(ser).unwrap(); + let result = $data.serialize(ser).unwrap(); assert_eq!(buffer, $expected); + assert_eq!(result, $result); } }; } @@ -587,51 +617,49 @@ pub(super) mod tests { } // Primitives is serialized in the same way as for SimpleTypeSerializer - serialize_as!(false_: false => "false"); - serialize_as!(true_: true => "true"); + serialize_as!(false_: false => "false", Text); + serialize_as!(true_: true => "true", Text); - 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!(i8_: -42i8 => "-42", Text); + serialize_as!(i16_: -4200i16 => "-4200", Text); + serialize_as!(i32_: -42000000i32 => "-42000000", Text); + serialize_as!(i64_: -42000000000000i64 => "-42000000000000", Text); + serialize_as!(isize_: -42000000000000isize => "-42000000000000", Text); - 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"); + serialize_as!(u8_: 42u8 => "42", Text); + serialize_as!(u16_: 4200u16 => "4200", Text); + serialize_as!(u32_: 42000000u32 => "42000000", Text); + serialize_as!(u64_: 42000000000000u64 => "42000000000000", Text); + serialize_as!(usize_: 42000000000000usize => "42000000000000", Text); serde_if_integer128! { - serialize_as!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); - serialize_as!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + serialize_as!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000", Text); + serialize_as!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000", Text); } - serialize_as!(f32_: 4.2f32 => "4.2"); - serialize_as!(f64_: 4.2f64 => "4.2"); + serialize_as!(f32_: 4.2f32 => "4.2", Text); + serialize_as!(f64_: 4.2f64 => "4.2", Text); - 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!(char_non_escaped: 'h' => "h", SensitiveText); + serialize_as!(char_lt: '<' => "<", SensitiveText); + serialize_as!(char_gt: '>' => ">", SensitiveText); + serialize_as!(char_amp: '&' => "&", SensitiveText); + serialize_as!(char_apos: '\'' => "'", SensitiveText); + serialize_as!(char_quot: '"' => """, SensitiveText); + serialize_as!(char_space: ' ' => " ", SensitiveText); - serialize_as!(str_non_escaped: "non-escaped string" => "non-escaped string"); - serialize_as!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + serialize_as!(str_non_escaped: "non-escaped string" => "non-escaped string", SensitiveText); + serialize_as!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>", SensitiveText); err!(bytes: Bytes(b"<\"escaped & bytes'>") => Unsupported("`serialize_bytes` not supported yet")); - serialize_as!(option_none: Option::::None => ""); - serialize_as!(option_some: Some("non-escaped string") => "non-escaped string"); - serialize_as!(option_some_empty_str: Some("") => ""); + serialize_as!(option_none: Option::::None => "", SensitiveNothing); + serialize_as!(option_some: Some("non-escaped string") => "non-escaped string", SensitiveText); + serialize_as!(option_some_empty_str: Some("") => "", SensitiveText); - serialize_as!(unit: () => ""); - serialize_as!(unit_struct: Unit => ""); - serialize_as!(unit_struct_escaped: UnitEscaped => ""); + serialize_as!(unit: () => "", Nothing); + serialize_as!(unit_struct: Unit => "", Nothing); + serialize_as!(unit_struct_escaped: UnitEscaped => "", Nothing); // Unlike SimpleTypeSerializer, enumeration values serialized as tags serialize_as!(enum_unit: Enum::Unit => ""); @@ -639,19 +667,19 @@ pub(super) mod tests { => Unsupported("character `<` is not allowed at the start of an XML name `<\"&'>`")); // Newtypes recursively applies ContentSerializer - serialize_as!(newtype: Newtype(42) => "42"); + serialize_as!(newtype: Newtype(42) => "42", Text); serialize_as!(enum_newtype: Enum::Newtype(42) => "42"); // Note that sequences of primitives serialized without delimiters! - serialize_as!(seq: vec![1, 2, 3] => "123"); - serialize_as!(seq_empty: Vec::::new() => ""); + serialize_as!(seq: vec![1, 2, 3] => "123", Text); + serialize_as!(seq_empty: Vec::::new() => "", SensitiveNothing); serialize_as!(tuple: ("<\"&'>", "with\t\r\n spaces", 3usize) => "<"&'>\ with\t\r\n spaces\ - 3"); + 3", Text); serialize_as!(tuple_struct: Tuple("first", 42) => "first\ - 42"); + 42", Text); serialize_as!(enum_tuple: Enum::Tuple("first", 42) => "first\ 42"); @@ -746,8 +774,6 @@ pub(super) mod tests { 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"); @@ -873,8 +899,6 @@ pub(super) mod tests { 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"); @@ -979,6 +1003,9 @@ pub(super) mod tests { /// Checks that given `$data` successfully serialized as `$expected` macro_rules! serialize_as { ($name:ident: $data:expr => $expected:expr) => { + serialize_as!($name: $data => $expected, WriteResult::Element); + }; + ($name:ident: $data:expr => $expected:expr, $result:expr) => { #[test] fn $name() { let mut buffer = String::new(); @@ -990,8 +1017,9 @@ pub(super) mod tests { expand_empty_elements: false, }; - $data.serialize(ser).unwrap(); + let result = $data.serialize(ser).unwrap(); assert_eq!(buffer, $expected); + assert_eq!(result, $result); } }; } @@ -1026,50 +1054,48 @@ pub(super) mod tests { }; } - serialize_as!(false_: false => "false"); - serialize_as!(true_: true => "true"); + serialize_as!(false_: false => "false", Text); + serialize_as!(true_: true => "true", Text); - 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!(i8_: -42i8 => "-42", Text); + serialize_as!(i16_: -4200i16 => "-4200", Text); + serialize_as!(i32_: -42000000i32 => "-42000000", Text); + serialize_as!(i64_: -42000000000000i64 => "-42000000000000", Text); + serialize_as!(isize_: -42000000000000isize => "-42000000000000", Text); - 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"); + serialize_as!(u8_: 42u8 => "42", Text); + serialize_as!(u16_: 4200u16 => "4200", Text); + serialize_as!(u32_: 42000000u32 => "42000000", Text); + serialize_as!(u64_: 42000000000000u64 => "42000000000000", Text); + serialize_as!(usize_: 42000000000000usize => "42000000000000", Text); serde_if_integer128! { - serialize_as!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000"); - serialize_as!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000"); + serialize_as!(i128_: -420000000000000000000000000000i128 => "-420000000000000000000000000000", Text); + serialize_as!(u128_: 420000000000000000000000000000u128 => "420000000000000000000000000000", Text); } - serialize_as!(f32_: 4.2f32 => "4.2"); - serialize_as!(f64_: 4.2f64 => "4.2"); + serialize_as!(f32_: 4.2f32 => "4.2", Text); + serialize_as!(f64_: 4.2f64 => "4.2", Text); - 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!(char_non_escaped: 'h' => "h", SensitiveText); + serialize_as!(char_lt: '<' => "<", SensitiveText); + serialize_as!(char_gt: '>' => ">", SensitiveText); + serialize_as!(char_amp: '&' => "&", SensitiveText); + serialize_as!(char_apos: '\'' => "'", SensitiveText); + serialize_as!(char_quot: '"' => """, SensitiveText); + serialize_as!(char_space: ' ' => " ", SensitiveText); - serialize_as!(str_non_escaped: "non-escaped string" => "non-escaped string"); - serialize_as!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>"); + serialize_as!(str_non_escaped: "non-escaped string" => "non-escaped string", SensitiveText); + serialize_as!(str_escaped: "<\"escaped & string'>" => "<"escaped & string'>", SensitiveText); err!(bytes: Bytes(b"<\"escaped & bytes'>") => Unsupported("`serialize_bytes` not supported yet")); - serialize_as!(option_none: Option::::None => ""); + serialize_as!(option_none: Option::::None => "", SensitiveNothing); serialize_as!(option_some: Some(Enum::Unit) => ""); - serialize_as!(unit: () => ""); - serialize_as!(unit_struct: Unit => ""); - serialize_as!(unit_struct_escaped: UnitEscaped => ""); + serialize_as!(unit: () => "", Nothing); + serialize_as!(unit_struct: Unit => "", Nothing); + serialize_as!(unit_struct_escaped: UnitEscaped => "", Nothing); // Unlike SimpleTypeSerializer, enumeration values serialized as tags serialize_as!(enum_unit: Enum::Unit => ""); @@ -1077,22 +1103,17 @@ pub(super) mod tests { => Unsupported("character `<` is not allowed at the start of an XML name `<\"&'>`")); // Newtypes recursively applies ContentSerializer - serialize_as!(newtype: Newtype(42) => "42"); + serialize_as!(newtype: Newtype(42) => "42", Text); 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() => ""); + // Note that sequences of primitives serialized without delimiters! + serialize_as!(seq: vec![1, 2, 3] => "123", Text); + serialize_as!(seq_empty: Vec::::new() => "", SensitiveNothing); 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"); + => "<"&'>\ + with\t\r\n spaces\ + 3", Text); + serialize_as!(tuple_struct: Tuple("first", 42) => "first42", Text); serialize_as!(enum_tuple: Enum::Tuple("first", 42) => "first\n\ 42"); @@ -1131,9 +1152,7 @@ pub(super) mod tests { after: "answer", } => "\n \ - answer\n \ - 42 42\n \ - answer\n\ + answer42 42answer\n\ "); } @@ -1143,18 +1162,6 @@ pub(super) mod tests { 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 { @@ -1163,9 +1170,9 @@ pub(super) mod tests { after: "answer", } => concat!( - "\n answer\n ", + "\n answer", $expected, - "\n answer\n", + "answer\n", )); }; } @@ -1199,8 +1206,6 @@ pub(super) mod tests { 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"); @@ -1214,13 +1219,13 @@ pub(super) mod tests { } => Unsupported("`serialize_bytes` not supported yet")); - text!(option_none: Option::<&str>::None); + text!(option_none: Option::<&str>::None => ""); text!(option_some: Some("non-escaped string") => "non-escaped string"); - text!(option_some_empty_str: Some("")); + text!(option_some_empty_str: Some("") => ""); - text!(unit: ()); - text!(unit_struct: Unit); - text!(unit_struct_escaped: UnitEscaped); + text!(unit: () => ""); + text!(unit_struct: Unit => ""); + text!(unit_struct_escaped: UnitEscaped => ""); text!(enum_unit: Enum::Unit => "Unit"); text!(enum_unit_escaped: Enum::UnitEscaped => "<"&'>"); @@ -1237,7 +1242,7 @@ pub(super) mod tests { // 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!(seq_empty: Vec::::new() => ""); text!(tuple: ("<\"&'>", "with\t\n\r spaces", 3usize) => "<"&'> \ with spaces \ @@ -1282,18 +1287,6 @@ pub(super) mod tests { 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 { @@ -1302,9 +1295,9 @@ pub(super) mod tests { after: "answer", } => concat!( - "\n answer\n ", + "\n answer", $expected, - "\n answer\n", + "answer\n", )); }; } @@ -1338,8 +1331,6 @@ pub(super) mod tests { 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"); @@ -1353,15 +1344,15 @@ pub(super) mod tests { } => Unsupported("`serialize_bytes` not supported yet")); - value!(option_none: Option::<&str>::None); + value!(option_none: Option::<&str>::None => ""); value!(option_some: Some("non-escaped string") => "non-escaped string"); - value!(option_some_empty_str: Some("")); + value!(option_some_empty_str: Some("") => ""); - value!(unit: ()); - value!(unit_struct: Unit); - value!(unit_struct_escaped: UnitEscaped); + value!(unit: () => "\n "); + value!(unit_struct: Unit => "\n "); + value!(unit_struct_escaped: UnitEscaped => "\n "); - value!(enum_unit: Enum::Unit => ""); + value!(enum_unit: Enum::Unit => "\n \n "); err!(enum_unit_escaped: SpecialEnum::Value { before: "answer", @@ -1371,19 +1362,20 @@ pub(super) mod tests { => 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!(enum_newtype: Enum::Newtype(42) => "\n 42\n "); // 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!(seq: vec![1, 2, 3] => "123"); + value!(seq_empty: Vec::::new() => ""); value!(tuple: ("<\"&'>", "with\t\n\r spaces", 3usize) - => "<"&'>\n \ - with\t\n\r spaces\n \ + => "<"&'>\ + with\t\n\r spaces\ 3"); - value!(tuple_struct: Tuple("first", 42) => "first\n 42"); + value!(tuple_struct: Tuple("first", 42) => "first42"); value!(enum_tuple: Enum::Tuple("first", 42) - => "first\n \ - 42"); + => "\n \ + first\n \ + 42\n "); // We cannot wrap map or struct in any container and should not // flatten it, so it is impossible to serialize maps and structs @@ -1403,11 +1395,12 @@ pub(super) mod tests { => Unsupported("serialization of struct `Struct` is not supported in `$value` field")); value!(enum_struct: Enum::Struct { key: "answer", val: (42, 42) } - => "\n \ + => "\n \ + \n \ answer\n \ 42\n \ 42\n \ - "); + \n "); } mod attributes { diff --git a/src/se/element.rs b/src/se/element.rs index 4121817f..5f9d3cf7 100644 --- a/src/se/element.rs +++ b/src/se/element.rs @@ -5,7 +5,7 @@ use crate::se::content::ContentSerializer; use crate::se::key::QNameSerializer; use crate::se::simple_type::{QuoteTarget, SimpleSeq, SimpleTypeSerializer}; use crate::se::text::TextSerializer; -use crate::se::{Indent, SeError, XmlName}; +use crate::se::{SeError, WriteResult, XmlName}; use serde::ser::{ Impossible, Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, Serializer, @@ -28,6 +28,8 @@ macro_rules! write_primitive { /// this serializer never uses variant names of enum variants, and because of that /// it is unable to serialize any enum values, except unit variants. /// +/// Returns the classification of the last written type. +/// /// This serializer is used for an ordinary fields in structs, which are not special /// fields named `$text` ([`TEXT_KEY`]) or `$value` ([`VALUE_KEY`]). `$text` field /// should be serialized using [`SimpleTypeSerializer`] and `$value` field should be @@ -63,7 +65,7 @@ pub struct ElementSerializer<'w, 'k, W: Write> { } impl<'w, 'k, W: Write> Serializer for ElementSerializer<'w, 'k, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; type SerializeSeq = Self; @@ -239,6 +241,7 @@ impl<'w, 'k, W: Write> Serializer for ElementSerializer<'w, 'k, W> { Ok(Struct { ser: self, children: String::new(), + write_indent: true, }) } @@ -263,7 +266,7 @@ impl<'w, 'k, W: Write> Serializer for ElementSerializer<'w, 'k, W> { } impl<'w, 'k, W: Write> SerializeSeq for ElementSerializer<'w, 'k, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> @@ -281,12 +284,12 @@ impl<'w, 'k, W: Write> SerializeSeq for ElementSerializer<'w, 'k, W> { #[inline] fn end(self) -> Result { - Ok(()) + Ok(WriteResult::Element) } } impl<'w, 'k, W: Write> SerializeTuple for ElementSerializer<'w, 'k, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; #[inline] @@ -304,7 +307,7 @@ impl<'w, 'k, W: Write> SerializeTuple for ElementSerializer<'w, 'k, W> { } impl<'w, 'k, W: Write> SerializeTupleStruct for ElementSerializer<'w, 'k, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; #[inline] @@ -330,11 +333,11 @@ pub enum Tuple<'w, 'k, W: Write> { /// Serialize each tuple field as an element Element(ElementSerializer<'w, 'k, W>), /// Serialize tuple as an `xs:list`: space-delimited content of fields - Text(SimpleSeq<'k, &'w mut W>), + Text(SimpleSeq<&'w mut W>), } impl<'w, 'k, W: Write> SerializeTupleVariant for Tuple<'w, 'k, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; #[inline] @@ -352,7 +355,9 @@ impl<'w, 'k, W: Write> SerializeTupleVariant for Tuple<'w, 'k, W> { fn end(self) -> Result { match self { Self::Element(ser) => SerializeTuple::end(ser), - Self::Text(ser) => SerializeTuple::end(ser).map(|_| ()), + // Do not write indent after `$text` fields because it may be interpreted as + // part of content when deserialize + Self::Text(ser) => SerializeTuple::end(ser).map(|_| WriteResult::SensitiveText), } } } @@ -362,6 +367,8 @@ impl<'w, 'k, W: Write> SerializeTupleVariant for Tuple<'w, 'k, W> { /// A serializer for struct variants, which serializes the struct contents inside /// of wrapping tags (`<${tag}>...`). /// +/// Returns the classification of the last written type. +/// /// Serialization of each field depends on it representation: /// - attributes written directly to the higher serializer /// - elements buffered into internal buffer and at the end written into higher @@ -373,6 +380,8 @@ pub struct Struct<'w, 'k, W: Write> { // attributes should be listed first. Fail, if attribute encountered after // element. Use feature to configure children: String, + /// Whether need to write indent after the last written field + write_indent: bool, } impl<'w, 'k, W: Write> Struct<'w, 'k, W> { @@ -407,7 +416,6 @@ impl<'w, 'k, W: Write> Struct<'w, '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('"')?; @@ -433,26 +441,32 @@ impl<'w, 'k, W: Write> Struct<'w, 'k, W> { writer: &mut self.children, level: self.ser.ser.level, indent: self.ser.ser.indent.borrow(), - write_indent: true, + // If previous field does not require indent, do not write it + write_indent: self.write_indent, expand_empty_elements: self.ser.ser.expand_empty_elements, }; if key == TEXT_KEY { value.serialize(TextSerializer(ser.into_simple_type_serializer()))?; + // Text was written so we don't need to indent next field + self.write_indent = false; } else if key == VALUE_KEY { - value.serialize(ser)?; + // If element was written then we need to indent next field unless it is a text field + self.write_indent = value.serialize(ser)?.allow_indent(); } else { value.serialize(ElementSerializer { key: XmlName::try_from(key)?, ser, })?; + // Element was written so we need to indent next field unless it is a text field + self.write_indent = true; } Ok(()) } } impl<'w, 'k, W: Write> SerializeStruct for Struct<'w, 'k, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> @@ -477,18 +491,20 @@ impl<'w, 'k, W: Write> SerializeStruct for Struct<'w, 'k, W> { 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)?; + if self.write_indent { + self.ser.ser.indent.write_indent(&mut self.ser.ser.writer)?; + } self.ser.ser.writer.write_str("')?; } - Ok(()) + Ok(WriteResult::Element) } } impl<'w, 'k, W: Write> SerializeStructVariant for Struct<'w, 'k, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; #[inline] @@ -526,7 +542,7 @@ impl<'w, 'k, W: Write> Map<'w, 'k, W> { } impl<'w, 'k, W: Write> SerializeMap for Map<'w, 'k, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> @@ -623,8 +639,9 @@ mod tests { key: XmlName("root"), }; - $data.serialize(ser).unwrap(); + let result = $data.serialize(ser).unwrap(); assert_eq!(buffer, $expected); + assert_eq!(result, WriteResult::Element); } }; } @@ -793,8 +810,6 @@ mod tests { 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"); @@ -920,8 +935,6 @@ mod tests { 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"); @@ -1051,8 +1064,6 @@ mod tests { 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"); @@ -1157,8 +1168,6 @@ mod tests { 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"); @@ -1309,7 +1318,8 @@ mod tests { use crate::writer::Indentation; use pretty_assertions::assert_eq; - /// Checks that given `$data` successfully serialized as `$expected` + /// Checks that given `$data` successfully serialized as `$expected`. + /// Writes `$data` using [`ElementSerializer`] with indent of two spaces. macro_rules! serialize_as { ($name:ident: $data:expr => $expected:expr) => { #[test] @@ -1326,8 +1336,9 @@ mod tests { key: XmlName("root"), }; - $data.serialize(ser).unwrap(); + let result = $data.serialize(ser).unwrap(); assert_eq!(buffer, $expected); + assert_eq!(result, WriteResult::Element); } }; } @@ -1394,8 +1405,6 @@ mod tests { 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"); @@ -1460,13 +1469,15 @@ mod tests { macro_rules! text { ($name:ident: $data:expr) => { serialize_as!($name: + // Serialization started from ElementSerializer::serialize_map BTreeMap::from([("$text", $data)]) => ""); }; ($name:ident: $data:expr => $expected:literal) => { serialize_as!($name: + // Serialization started from ElementSerializer::serialize_map BTreeMap::from([("$text", $data)]) - => concat!("\n ", $expected,"\n")); + => concat!("", $expected,"")); }; } @@ -1499,8 +1510,6 @@ mod tests { 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"); @@ -1582,29 +1591,18 @@ mod tests { 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: + // Serialization started from ElementSerializer::serialize_struct Text { before: "answer", content: $data, after: "answer", } => concat!( - "\n answer\n ", + "\n answer", $expected, - "\n answer\n", + "answer\n", )); }; } @@ -1638,8 +1636,6 @@ mod tests { 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"); @@ -1653,13 +1649,13 @@ mod tests { } => Unsupported("`serialize_bytes` not supported yet")); - text!(option_none: Option::<&str>::None); + text!(option_none: Option::<&str>::None => ""); text!(option_some: Some("non-escaped string") => "non-escaped string"); - text!(option_some_empty_str: Some("")); + text!(option_some_empty_str: Some("") => ""); - text!(unit: ()); - text!(unit_struct: Unit); - text!(unit_struct_escaped: UnitEscaped); + text!(unit: () => ""); + text!(unit_struct: Unit => ""); + text!(unit_struct_escaped: UnitEscaped => ""); text!(enum_unit: Enum::Unit => "Unit"); text!(enum_unit_escaped: Enum::UnitEscaped => "<"&'>"); @@ -1676,7 +1672,7 @@ mod tests { // 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!(seq_empty: Vec::::new() => ""); text!(tuple: ("<\"&'>", "with\t\n\r spaces", 3usize) => "<"&'> \ with spaces \ @@ -1730,13 +1726,15 @@ mod tests { macro_rules! value { ($name:ident: $data:expr) => { serialize_as!($name: + // Serialization started from ElementSerializer::serialize_map BTreeMap::from([("$value", $data)]) => ""); }; ($name:ident: $data:expr => $expected:literal) => { serialize_as!($name: + // Serialization started from ElementSerializer::serialize_map BTreeMap::from([("$value", $data)]) - => concat!("\n ", $expected,"\n")); + => concat!("", $expected,"")); }; } @@ -1769,8 +1767,6 @@ mod tests { 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"); @@ -1788,24 +1784,25 @@ mod tests { value!(unit_struct: Unit); value!(unit_struct_escaped: UnitEscaped); - value!(enum_unit: Enum::Unit => ""); + value!(enum_unit: Enum::Unit => "\n \n"); 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!(enum_newtype: Enum::Newtype(42) => "\n 42\n"); - value!(seq: vec![1, 2, 3] => "1\n 2\n 3"); + value!(seq: vec![1, 2, 3] => "123"); value!(seq_empty: Vec::::new()); value!(tuple: ("<\"&'>", "with\t\n\r spaces", 3usize) - => "<"&'>\n \ - with\t\n\r spaces\n \ + => "<"&'>\ + with\t\n\r spaces\ 3"); - value!(tuple_struct: Tuple("first", 42) => "first\n 42"); + value!(tuple_struct: Tuple("first", 42) => "first42"); value!(enum_tuple: Enum::Tuple("first", 42) - => "first\n \ - 42"); + => "\n \ + first\n \ + 42\n"); // We cannot wrap map or struct in any container and should not // flatten it, so it is impossible to serialize maps and structs @@ -1817,11 +1814,12 @@ mod tests { => Unsupported("serialization of struct `Struct` is not supported in `$value` field")); value!(enum_struct: Enum::Struct { key: "answer", val: (42, 42) } - => "\n \ + => "\n \ + \n \ answer\n \ 42\n \ 42\n \ - "); + \n"); } /// `$value` field inside a struct @@ -1830,29 +1828,18 @@ mod tests { 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: + // Serialization started from ElementSerializer::serialize_struct Value { before: "answer", content: $data, after: "answer", } => concat!( - "\n answer\n ", + "\n answer", $expected, - "\n answer\n", + "answer\n", )); }; } @@ -1886,8 +1873,6 @@ mod tests { 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"); @@ -1901,15 +1886,15 @@ mod tests { } => Unsupported("`serialize_bytes` not supported yet")); - value!(option_none: Option::<&str>::None); + value!(option_none: Option::<&str>::None => ""); value!(option_some: Some("non-escaped string") => "non-escaped string"); - value!(option_some_empty_str: Some("")); + value!(option_some_empty_str: Some("") => ""); - value!(unit: ()); - value!(unit_struct: Unit); - value!(unit_struct_escaped: UnitEscaped); + value!(unit: () => "\n "); + value!(unit_struct: Unit => "\n "); + value!(unit_struct_escaped: UnitEscaped => "\n "); - value!(enum_unit: Enum::Unit => ""); + value!(enum_unit: Enum::Unit => "\n \n "); err!(enum_unit_escaped: Value { before: "answer", @@ -1919,19 +1904,20 @@ mod tests { => 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!(enum_newtype: Enum::Newtype(42) => "\n 42\n "); // 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!(seq: vec![1, 2, 3] => "123"); + value!(seq_empty: Vec::::new() => ""); value!(tuple: ("<\"&'>", "with\t\n\r spaces", 3usize) - => "<"&'>\n \ - with\t\n\r spaces\n \ + => "<"&'>\ + with\t\n\r spaces\ 3"); - value!(tuple_struct: Tuple("first", 42) => "first\n 42"); + value!(tuple_struct: Tuple("first", 42) => "first42"); value!(enum_tuple: Enum::Tuple("first", 42) - => "first\n \ - 42"); + => "\n \ + first\n \ + 42\n "); // We cannot wrap map or struct in any container and should not // flatten it, so it is impossible to serialize maps and structs @@ -1951,11 +1937,12 @@ mod tests { => Unsupported("serialization of struct `Struct` is not supported in `$value` field")); value!(enum_struct: Enum::Struct { key: "answer", val: (42, 42) } - => "\n \ + => "\n \ + \n \ answer\n \ 42\n \ 42\n \ - "); + \n "); } } @@ -2059,8 +2046,9 @@ mod tests { key: XmlName("root"), }; - $data.serialize(ser).unwrap(); + let result = $data.serialize(ser).unwrap(); assert_eq!(buffer, $expected); + assert_eq!(result, WriteResult::Element); } }; } diff --git a/src/se/mod.rs b/src/se/mod.rs index b8c09a09..db2f8bbb 100644 --- a/src/se/mod.rs +++ b/src/se/mod.rs @@ -91,6 +91,8 @@ use std::str::from_utf8; /// Serialize struct into a `Write`r. /// +/// Returns the classification of the last written type. +/// /// # Examples /// /// ``` @@ -124,7 +126,7 @@ use std::str::from_utf8; /// " /// ); /// ``` -pub fn to_writer(mut writer: W, value: &T) -> Result<(), SeError> +pub fn to_writer(mut writer: W, value: &T) -> Result where W: Write, T: ?Sized + Serialize, @@ -177,6 +179,8 @@ where /// Serialize struct into a `Write`r using specified root tag name. /// `root_tag` should be valid [XML name], otherwise error is returned. /// +/// Returns the classification of the last written type. +/// /// # Examples /// /// ``` @@ -210,7 +214,11 @@ where /// ``` /// /// [XML name]: https://www.w3.org/TR/xml11/#NT-Name -pub fn to_writer_with_root(mut writer: W, root_tag: &str, value: &T) -> Result<(), SeError> +pub fn to_writer_with_root( + mut writer: W, + root_tag: &str, + value: &T, +) -> Result where W: Write, T: ?Sized + Serialize, @@ -309,6 +317,34 @@ pub enum QuoteLevel { Minimal, } +/// Classification of the type written by the serializer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WriteResult { + /// Text with insignificant spaces was written, for example a number. Adding indent to the + /// serialized data does not change meaning of the data. + Text, + /// The XML tag was written. Adding indent to the serialized data does not change meaning of the data. + Element, + /// Nothing was written (i. e. serialized type not represented in XML a all). Adding indent to the + /// serialized data does not change meaning of the data. This is returned for units, unit structs + /// and unit variants. + Nothing, + /// Text with significant spaces was written, for example a string. Adding indent to the + /// serialized data may change meaning of the data. + SensitiveText, + /// `None` was serialized and nothing was written. `None` does not represented in XML, + /// but adding indent after it may change meaning of the data. + SensitiveNothing, +} + +impl WriteResult { + /// Returns `true` if indent should be written after the object (if configured) and `false` otherwise. + #[inline] + pub fn allow_indent(&self) -> bool { + matches!(self, Self::Element | Self::Nothing) + } +} + //////////////////////////////////////////////////////////////////////////////////////////////////// /// Implements serialization method by forwarding it to the serializer created by @@ -444,7 +480,9 @@ impl<'i> Indent<'i> { //////////////////////////////////////////////////////////////////////////////////////////////////// -/// A Serializer +/// A Serializer. +/// +/// Returns the classification of the last written type. pub struct Serializer<'w, 'r, W: Write> { ser: ContentSerializer<'w, 'r, W>, /// Name of the root tag. If not specified, deduced from the structure name @@ -615,7 +653,7 @@ impl<'w, 'r, W: Write> Serializer<'w, 'r, W> { } impl<'w, 'r, W: Write> ser::Serializer for Serializer<'w, 'r, W> { - type Ok = (); + type Ok = WriteResult; type Error = SeError; type SerializeSeq = ElementSerializer<'w, 'r, W>; @@ -651,7 +689,11 @@ impl<'w, 'r, W: Write> ser::Serializer for Serializer<'w, 'r, W> { forward!(serialize_bytes(&[u8])); fn serialize_none(self) -> Result { - Ok(()) + // Do not write indent after `Option` field with `None` value, because + // this can be `Option`. Unfortunately, we do not known what the + // type the option contains, so have no chance to adapt our behavior to it. + // The safe variant is not to write indent + Ok(WriteResult::SensitiveNothing) } fn serialize_some(self, value: &T) -> Result { @@ -704,7 +746,9 @@ impl<'w, 'r, W: Write> ser::Serializer for Serializer<'w, 'r, W> { ) -> Result { if variant == TEXT_KEY { value.serialize(self.ser.into_simple_type_serializer())?; - Ok(()) + // Do not write indent after `$text` variant because it may be interpreted as + // part of content when deserialize + Ok(WriteResult::SensitiveText) } else { let ser = ElementSerializer { ser: self.ser, diff --git a/src/se/simple_type.rs b/src/se/simple_type.rs index dbdfe277..c7336ce6 100644 --- a/src/se/simple_type.rs +++ b/src/se/simple_type.rs @@ -4,7 +4,7 @@ //! [as defined]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition use crate::escape::_escape; -use crate::se::{Indent, QuoteLevel, SeError}; +use crate::se::{QuoteLevel, SeError}; use serde::ser::{ Impossible, Serialize, SerializeSeq, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant, Serializer, @@ -179,22 +179,18 @@ macro_rules! write_atomic { /// /// [item]: https://www.w3.org/TR/xmlschema11-1/#std-item_type_definition /// [simple type]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition -pub struct AtomicSerializer<'i, W: Write> { +pub struct AtomicSerializer { pub writer: W, pub target: QuoteTarget, /// Defines which XML characters need to be escaped pub level: QuoteLevel, - /// When `Some`, the indent that should be written before the content - /// if content is not an empty string. - /// When `None` an `xs:list` delimiter (a space) should be written - pub(crate) indent: Option>, + /// When `true` an `xs:list` delimiter (a space) should be written + pub(crate) write_delimiter: bool, } -impl<'i, W: Write> AtomicSerializer<'i, W> { +impl AtomicSerializer { fn write_str(&mut self, value: &str) -> Result<(), SeError> { - if let Some(indent) = self.indent.as_mut() { - indent.write_indent(&mut self.writer)?; - } else { + if self.write_delimiter { // TODO: Customization point -- possible non-XML compatible extension to specify delimiter char self.writer.write_char(' ')?; } @@ -202,7 +198,7 @@ impl<'i, W: Write> AtomicSerializer<'i, W> { } } -impl<'i, W: Write> Serializer for AtomicSerializer<'i, W> { +impl Serializer for AtomicSerializer { type Ok = bool; type Error = SeError; @@ -401,31 +397,28 @@ impl<'i, W: Write> Serializer for AtomicSerializer<'i, W> { /// - CDATA content (`<...>`) /// /// [simple types]: https://www.w3.org/TR/xmlschema11-1/#Simple_Type_Definition -pub struct SimpleTypeSerializer<'i, W: Write> { +pub struct SimpleTypeSerializer { /// 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<'i, W: Write> SimpleTypeSerializer<'i, W> { +impl SimpleTypeSerializer { fn write_str(&mut self, value: &str) -> Result<(), SeError> { - self.indent.write_indent(&mut self.writer)?; Ok(self.writer.write_str(value)?) } } -impl<'i, W: Write> Serializer for SimpleTypeSerializer<'i, W> { +impl Serializer for SimpleTypeSerializer { type Ok = W; type Error = SeError; - type SerializeSeq = SimpleSeq<'i, W>; - type SerializeTuple = SimpleSeq<'i, W>; - type SerializeTupleStruct = SimpleSeq<'i, W>; + type SerializeSeq = SimpleSeq; + type SerializeTuple = SimpleSeq; + type SerializeTupleStruct = SimpleSeq; type SerializeTupleVariant = Impossible; type SerializeMap = Impossible; type SerializeStruct = Impossible; @@ -470,7 +463,6 @@ impl<'i, W: Write> Serializer for SimpleTypeSerializer<'i, W> { writer: self.writer, target: self.target, level: self.level, - indent: self.indent, is_empty: true, }) } @@ -535,17 +527,15 @@ impl<'i, W: Write> Serializer for SimpleTypeSerializer<'i, W> { } /// Serializer for a sequence of atomic values delimited by space -pub struct SimpleSeq<'i, W: Write> { +pub struct SimpleSeq { writer: W, target: QuoteTarget, level: QuoteLevel, - /// Indent that should be written before the content if content is not an empty string - indent: Indent<'i>, /// If `true`, nothing was written yet to the `writer` is_empty: bool, } -impl<'i, W: Write> SerializeSeq for SimpleSeq<'i, W> { +impl SerializeSeq for SimpleSeq { type Ok = W; type Error = SeError; @@ -553,17 +543,11 @@ impl<'i, W: Write> SerializeSeq for SimpleSeq<'i, W> { where T: ?Sized + Serialize, { - // Write indent for the first element and delimiter for others - let indent = if self.is_empty { - Some(self.indent.borrow()) - } else { - None - }; if value.serialize(AtomicSerializer { writer: &mut self.writer, target: self.target, level: self.level, - indent, + write_delimiter: !self.is_empty, })? { self.is_empty = false; } @@ -576,7 +560,7 @@ impl<'i, W: Write> SerializeSeq for SimpleSeq<'i, W> { } } -impl<'i, W: Write> SerializeTuple for SimpleSeq<'i, W> { +impl SerializeTuple for SimpleSeq { type Ok = W; type Error = SeError; @@ -594,7 +578,7 @@ impl<'i, W: Write> SerializeTuple for SimpleSeq<'i, W> { } } -impl<'i, W: Write> SerializeTupleStruct for SimpleSeq<'i, W> { +impl SerializeTupleStruct for SimpleSeq { type Ok = W; type Error = SeError; @@ -612,7 +596,7 @@ impl<'i, W: Write> SerializeTupleStruct for SimpleSeq<'i, W> { } } -impl<'i, W: Write> SerializeTupleVariant for SimpleSeq<'i, W> { +impl SerializeTupleVariant for SimpleSeq { type Ok = W; type Error = SeError; @@ -928,7 +912,7 @@ mod tests { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Some(Indent::None), + write_delimiter: false, }; let has_written = $data.serialize(ser).unwrap(); @@ -949,7 +933,7 @@ mod tests { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Some(Indent::None), + write_delimiter: false, }; match $data.serialize(ser).unwrap_err() { @@ -1047,7 +1031,6 @@ mod tests { writer: String::new(), target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Indent::None, }; let buffer = $data.serialize(ser).unwrap(); @@ -1067,7 +1050,6 @@ mod tests { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Indent::None, }; match $data.serialize(ser).unwrap_err() { @@ -1153,19 +1135,15 @@ mod tests { mod simple_seq { use super::*; - use crate::writer::Indentation; use pretty_assertions::assert_eq; #[test] fn empty_seq() { let mut buffer = String::new(); - let mut indent = Indentation::new(b'*', 2); - indent.grow(); let ser = SimpleSeq { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Indent::Owned(indent), is_empty: true, }; @@ -1176,13 +1154,10 @@ mod tests { #[test] fn all_items_empty() { let mut buffer = String::new(); - let mut indent = Indentation::new(b'*', 2); - indent.grow(); let mut ser = SimpleSeq { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Indent::Owned(indent), is_empty: true, }; @@ -1196,13 +1171,10 @@ mod tests { #[test] fn some_items_empty1() { let mut buffer = String::new(); - let mut indent = Indentation::new(b'*', 2); - indent.grow(); let mut ser = SimpleSeq { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Indent::Owned(indent), is_empty: true, }; @@ -1210,19 +1182,16 @@ mod tests { SerializeSeq::serialize_element(&mut ser, &1).unwrap(); SerializeSeq::serialize_element(&mut ser, "").unwrap(); SerializeSeq::end(ser).unwrap(); - assert_eq!(buffer, "\n**1"); + assert_eq!(buffer, "1"); } #[test] fn some_items_empty2() { let mut buffer = String::new(); - let mut indent = Indentation::new(b'*', 2); - indent.grow(); let mut ser = SimpleSeq { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Indent::Owned(indent), is_empty: true, }; @@ -1230,19 +1199,16 @@ mod tests { SerializeSeq::serialize_element(&mut ser, "").unwrap(); SerializeSeq::serialize_element(&mut ser, &2).unwrap(); SerializeSeq::end(ser).unwrap(); - assert_eq!(buffer, "\n**1 2"); + assert_eq!(buffer, "1 2"); } #[test] fn items() { let mut buffer = String::new(); - let mut indent = Indentation::new(b'*', 2); - indent.grow(); let mut ser = SimpleSeq { writer: &mut buffer, target: QuoteTarget::Text, level: QuoteLevel::Full, - indent: Indent::Owned(indent), is_empty: true, }; @@ -1250,7 +1216,7 @@ mod tests { SerializeSeq::serialize_element(&mut ser, &2).unwrap(); SerializeSeq::serialize_element(&mut ser, &3).unwrap(); SerializeSeq::end(ser).unwrap(); - assert_eq!(buffer, "\n**1 2 3"); + assert_eq!(buffer, "1 2 3"); } } } diff --git a/src/se/text.rs b/src/se/text.rs index 9dd30509..7094adba 100644 --- a/src/se/text.rs +++ b/src/se/text.rs @@ -23,16 +23,16 @@ macro_rules! write_primitive { /// This serializer a very similar to [`SimpleTypeSerializer`], but different /// from it in how it processes unit enum variants. Unlike [`SimpleTypeSerializer`] /// this serializer does not write anything for the unit variant. -pub struct TextSerializer<'i, W: Write>(pub SimpleTypeSerializer<'i, W>); +pub struct TextSerializer(pub SimpleTypeSerializer); -impl<'i, W: Write> Serializer for TextSerializer<'i, W> { +impl Serializer for TextSerializer { type Ok = W; type Error = SeError; - type SerializeSeq = SimpleSeq<'i, W>; - type SerializeTuple = SimpleSeq<'i, W>; - type SerializeTupleStruct = SimpleSeq<'i, W>; - type SerializeTupleVariant = SimpleSeq<'i, W>; + type SerializeSeq = SimpleSeq; + type SerializeTuple = SimpleSeq; + type SerializeTupleStruct = SimpleSeq; + type SerializeTupleVariant = SimpleSeq; type SerializeMap = Impossible; type SerializeStruct = Impossible; type SerializeStructVariant = Impossible; diff --git a/tests/serde-issues.rs b/tests/serde-issues.rs index 9781ae72..bac7d416 100644 --- a/tests/serde-issues.rs +++ b/tests/serde-issues.rs @@ -4,7 +4,7 @@ use pretty_assertions::assert_eq; use quick_xml::de::{from_reader, from_str}; -use quick_xml::se::{to_string, to_string_with_root}; +use quick_xml::se::{to_string, to_string_with_root, Serializer}; use serde::de::{Deserializer, IgnoredAny}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -475,6 +475,53 @@ fn issue580() { ); } +/// Regression test for https://github.com/tafia/quick-xml/issues/655 +#[test] +fn issue655() { + #[derive(Deserialize, Serialize, Debug)] + pub struct TextureCoordinates { + #[serde(rename = "@dimension")] + pub dimension: String, + #[serde(rename = "@channel")] + pub channel: String, + #[serde(rename = "$value")] + pub elements: String, + } + #[derive(Deserialize, Serialize, Debug)] + #[serde(rename_all = "PascalCase")] + pub struct VertexBuffer { + pub positions: String, + pub normals: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub texture_coordinates: Option, + } + + let mut buffer = String::new(); + let mut ser = Serializer::with_root(&mut buffer, None).unwrap(); + ser.indent(' ', 2); + ser.expand_empty_elements(true); + + let obj = VertexBuffer { + positions: "319.066 -881.28705 7.71589".into(), + normals: "-0.0195154 -0.21420999 0.976593".into(), + texture_coordinates: Some(TextureCoordinates { + dimension: "2D".into(), + channel: "0".into(), + elements: "752494 0.201033,0.773967 0.201033".into(), + }), + }; + obj.serialize(ser).unwrap(); + assert_eq!( + buffer, + "\ + + 319.066 -881.28705 7.71589 + -0.0195154 -0.21420999 0.976593 + 752494 0.201033,0.773967 0.201033 +" + ); +} + /// Regression test for https://github.com/tafia/quick-xml/issues/683. #[test] fn issue683() { diff --git a/tests/serde-se.rs b/tests/serde-se.rs index fc560628..ec73b4ee 100644 --- a/tests/serde-se.rs +++ b/tests/serde-se.rs @@ -1380,9 +1380,7 @@ mod without_root { float: 42.0, string: "answer" } - => "\n \ - 42\n \ - answer\n\ + => "42answer\n\ "); mod enum_ { @@ -1439,9 +1437,7 @@ mod without_root { float: 42.0, string: "answer" } - => "\n \ - 42\n \ - answer\n\ + => "42answer\n\ "); /// Test serialization of the specially named variant `$text` @@ -1544,9 +1540,7 @@ mod without_root { string: "answer" } => "\n \ - Text\n \ - 42\n \ - answer\n\ + Text42answer\n\ "); } @@ -1624,9 +1618,7 @@ mod without_root { } => "\n \ Text\n \ - \n \ - 42\n \ - answer\n \ + 42answer\n \ \n\ "); } @@ -1676,9 +1668,7 @@ mod without_root { float: 42.0, string: "answer" } - => "\n \ - 42\n \ - answer\n\ + => "42answer\n\ "); } } diff --git a/tests/writer-indentation.rs b/tests/writer-indentation.rs index f96f6063..baabd967 100644 --- a/tests/writer-indentation.rs +++ b/tests/writer-indentation.rs @@ -222,9 +222,7 @@ fn serializable() { 43 first element - second element - text - foo + second elementtextfoo "# );