Skip to content

Commit

Permalink
Add expand_empty_elements fn to Serializer (#620)
Browse files Browse the repository at this point in the history
* Add expand_empty_elements fn to Serializer

Setting this to true will serialize empty elements to
<element></element> instead of <element/>.

* Move the expand empty elements logic into write_empty

* Add doctest example for expand_empty_elements

* Add entry to CHANGELOG.md

* Link to issue instead of PR in changelog
  • Loading branch information
bahlo authored Jun 30, 2023
1 parent aca1f79 commit 05059fd
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [#609]: Added `Writer::write_serializable` to provide the capability to serialize
arbitrary types using serde when using the lower-level `Writer` API.
- [#615]: Added ability to set entity resolver when deserialize using borrowing reader.
- [#617]: Added ability to enforce the expansion of empty elements.

### Bug Fixes

Expand All @@ -26,6 +27,7 @@
[#604]: https://github.com/tafia/quick-xml/issue/604
[#609]: https://github.com/tafia/quick-xml/pull/609
[#615]: https://github.com/tafia/quick-xml/pull/615
[#617]: https://github.com/tafia/quick-xml/pull/617


## 0.29.0 -- 2023-06-13
Expand Down
22 changes: 19 additions & 3 deletions src/se/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ pub struct ContentSerializer<'w, 'i, W: Write> {
/// If `true`, then current indent will be written before writing the content,
/// but only if content is not empty.
pub write_indent: bool,
// If `true`, then empty elements will be serialized as `<element></element>`
// instead of `<element/>`.
pub expand_empty_elements: bool,
//TODO: add settings to disallow consequent serialization of primitives
}

Expand Down Expand Up @@ -85,16 +88,25 @@ impl<'w, 'i, W: Write> ContentSerializer<'w, 'i, W> {
level: self.level,
indent: self.indent.borrow(),
write_indent: self.write_indent,
expand_empty_elements: false,
}
}

/// Writes `name` as self-closed tag
#[inline]
pub(super) fn write_empty(mut self, name: XmlName) -> Result<(), DeError> {
self.write_indent()?;
self.writer.write_char('<')?;
self.writer.write_str(name.0)?;
self.writer.write_str("/>")?;
if self.expand_empty_elements {
self.writer.write_char('<')?;
self.writer.write_str(name.0)?;
self.writer.write_str("></")?;
self.writer.write_str(name.0)?;
self.writer.write_char('>')?;
} else {
self.writer.write_str("<")?;
self.writer.write_str(name.0)?;
self.writer.write_str("/>")?;
}
Ok(())
}

Expand Down Expand Up @@ -483,6 +495,7 @@ pub(super) mod tests {
level: QuoteLevel::Full,
indent: Indent::None,
write_indent: false,
expand_empty_elements: false,
};

$data.serialize(ser).unwrap();
Expand All @@ -503,6 +516,7 @@ pub(super) mod tests {
level: QuoteLevel::Full,
indent: Indent::None,
write_indent: false,
expand_empty_elements: false,
};

match $data.serialize(ser).unwrap_err() {
Expand Down Expand Up @@ -672,6 +686,7 @@ pub(super) mod tests {
level: QuoteLevel::Full,
indent: Indent::Owned(Indentation::new(b' ', 2)),
write_indent: false,
expand_empty_elements: false,
};

$data.serialize(ser).unwrap();
Expand All @@ -692,6 +707,7 @@ pub(super) mod tests {
level: QuoteLevel::Full,
indent: Indent::Owned(Indentation::new(b' ', 2)),
write_indent: false,
expand_empty_elements: false,
};

match $data.serialize(ser).unwrap_err() {
Expand Down
77 changes: 77 additions & 0 deletions src/se/element.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ impl<'w, 'k, W: Write> Struct<'w, 'k, W> {
level: self.ser.ser.level,
indent: self.ser.ser.indent.borrow(),
write_indent: true,
expand_empty_elements: false,
};

if key == TEXT_KEY {
Expand Down Expand Up @@ -574,6 +575,7 @@ mod tests {
level: QuoteLevel::Full,
indent: Indent::None,
write_indent: false,
expand_empty_elements: false,
},
key: XmlName("root"),
};
Expand All @@ -597,6 +599,7 @@ mod tests {
level: QuoteLevel::Full,
indent: Indent::None,
write_indent: false,
expand_empty_elements: false,
},
key: XmlName("root"),
};
Expand Down Expand Up @@ -1533,6 +1536,7 @@ mod tests {
level: QuoteLevel::Full,
indent: Indent::Owned(Indentation::new(b' ', 2)),
write_indent: false,
expand_empty_elements: false,
},
key: XmlName("root"),
};
Expand All @@ -1556,6 +1560,7 @@ mod tests {
level: QuoteLevel::Full,
indent: Indent::Owned(Indentation::new(b' ', 2)),
write_indent: false,
expand_empty_elements: false,
},
key: XmlName("root"),
};
Expand Down Expand Up @@ -2533,4 +2538,76 @@ mod tests {
</root>");
}
}

mod expand_empty_elements {
use super::*;
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 mut buffer = String::new();
let ser = ElementSerializer {
ser: ContentSerializer {
writer: &mut buffer,
level: QuoteLevel::Full,
indent: Indent::None,
write_indent: false,
expand_empty_elements: true,
},
key: XmlName("root"),
};

$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::None,
write_indent: false,
expand_empty_elements: 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!(option_some_empty: Some("") => "<root></root>");
serialize_as!(option_some_empty_str: Some("") => "<root></root>");

serialize_as!(unit: () => "<root></root>");
serialize_as!(unit_struct: Unit => "<root></root>");
serialize_as!(unit_struct_escaped: UnitEscaped => "<root></root>");

serialize_as!(enum_unit: Enum::Unit => "<Unit></Unit>");
err!(enum_unit_escaped: Enum::UnitEscaped
=> Unsupported("character `<` is not allowed at the start of an XML name `<\"&'>`"));
}
}
35 changes: 35 additions & 0 deletions src/se/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ impl<'w, 'r, W: Write> Serializer<'w, 'r, W> {
level: QuoteLevel::Full,
indent: Indent::None,
write_indent: false,
expand_empty_elements: false,
},
root_tag: None,
}
Expand Down Expand Up @@ -522,11 +523,45 @@ impl<'w, 'r, W: Write> Serializer<'w, 'r, W> {
level: QuoteLevel::Full,
indent: Indent::None,
write_indent: false,
expand_empty_elements: false,
},
root_tag: root_tag.map(|tag| XmlName::try_from(tag)).transpose()?,
})
}

/// Enable or disable expansion of empty elements. Defaults to `false`.
///
/// # Examples
///
/// ```
/// # use pretty_assertions::assert_eq;
/// # use serde::Serialize;
/// # use quick_xml::se::Serializer;
///
/// #[derive(Debug, PartialEq, Serialize)]
/// struct Struct {
/// question: Option<String>,
/// }
///
/// let mut buffer = String::new();
/// let mut ser = Serializer::new(&mut buffer);
/// ser.expand_empty_elements(true);
///
/// let data = Struct {
/// question: None,
/// };
///
/// data.serialize(ser).unwrap();
/// assert_eq!(
/// buffer,
/// "<Struct><question></question></Struct>"
/// );
/// ```
pub fn expand_empty_elements(&mut self, expand: bool) -> &mut Self {
self.ser.expand_empty_elements = expand;
self
}

/// Configure indent for a serializer
pub fn indent(&mut self, indent_char: char, indent_size: usize) -> &mut Self {
self.ser.indent = Indent::Owned(Indentation::new(indent_char as u8, indent_size));
Expand Down

0 comments on commit 05059fd

Please sign in to comment.