Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic implementation of the ElementWriter concept #278

Merged
merged 2 commits into from
Mar 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl<'a> BytesStart<'a> {
///
/// # Example
///
/// ```
/// ```rust
/// # use quick_xml::{Error, Writer};
/// use quick_xml::events::{BytesStart, Event};
///
Expand Down Expand Up @@ -315,10 +315,13 @@ impl<'a> BytesStart<'a> {
}

/// Try to get an attribute
pub fn try_get_attribute(&'a self, attr_name: &[u8]) -> Result<Option<Attribute<'a>>> {
pub fn try_get_attribute<N: AsRef<[u8]> + Sized>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attribute names don't need to be escaped right? This would let users pass &str, &[u8], or Vec<u8>.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, names don't need escaping.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed one difference between a library I'm writing and an existing one that uses libxml2 - the one based on libxml2 seems to strip tab characters \t out of attribute values. I can't find anything in the spec that mentions that nor can I find it in libxml2 though - do you know whether that might be required?

&'a self,
attr_name: N,
) -> Result<Option<Attribute<'a>>> {
for a in self.attributes() {
let a = a?;
if a.key == attr_name {
if a.key == attr_name.as_ref() {
return Ok(Some(a));
}
}
Expand Down Expand Up @@ -802,7 +805,7 @@ pub enum Event<'a> {

impl<'a> Event<'a> {
/// Converts the event to an owned version, untied to the lifetime of
/// buffer used when reading but incurring a new, seperate allocation.
/// buffer used when reading but incurring a new, separate allocation.
pub fn into_owned(self) -> Event<'static> {
match self {
Event::Start(e) => Event::Start(e.into_owned()),
Expand Down
248 changes: 217 additions & 31 deletions src/writer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! A module to handle `Writer`

use crate::{errors::Error, errors::Result, events::BytesEnd, events::BytesStart, events::Event};
use crate::errors::{Error, Result};
use crate::events::{attributes::Attribute, BytesStart, BytesText, Event};
use std::io::Write;

/// XML writer.
Expand All @@ -10,7 +11,6 @@ use std::io::Write;
/// # Examples
///
/// ```rust
/// # extern crate quick_xml;
/// # fn main() {
/// use quick_xml::{Reader, Writer};
/// use quick_xml::events::{Event, BytesEnd, BytesStart};
Expand Down Expand Up @@ -169,20 +169,134 @@ impl<W: Write> Writer<W> {
Ok(())
}

/// Write event nested in another level:
/// ```xml
/// <parent_name>{event}</parent_name>
/// Provides a simple, high-level API for writing XML elements.
///
/// Returns an [ElementWriter] that simplifies setting attributes and writing content inside the element.
///
/// # Example
///
/// ```rust
/// # use quick_xml::Result;
/// # fn main() -> Result<()> {
/// use quick_xml::{Error, Writer};
/// use quick_xml::events::{BytesStart, BytesText, Event};
/// use std::io::Cursor;
///
/// let mut writer = Writer::new(Cursor::new(Vec::new()));
///
/// // writes <tag attr1="value1"/>
/// writer.create_element("tag")
/// .with_attribute(("attr1", "value1")) // chain `with_attribute()` calls to add many attributes
/// .write_empty()?;
///
/// // writes <tag attr1="value1" attr2="value2">with some text inside</tag>
/// writer.create_element("tag")
/// .with_attributes(vec![("attr1", "value1"), ("attr2", "value2")].into_iter()) // or add attributes from an iterator
/// .write_text_content(BytesText::from_plain_str("with some text inside"))?;
///
/// // writes <tag><fruit quantity="0">apple</fruit><fruit quantity="1">orange</fruit></tag>
/// writer.create_element("tag")
/// .write_inner_content(|writer| {
/// let fruits = ["apple", "orange"];
/// for (quant, item) in fruits.iter().enumerate() {
/// writer
/// .create_element("fruit")
/// .with_attribute(("quantity", quant.to_string().as_str()))
/// .write_text_content(BytesText::from_plain_str(item))?;
/// }
/// Ok(())
/// })?;
/// # Ok(())
/// # }
/// ```
pub fn write_nested_event<'a, N: AsRef<[u8]>, E: AsRef<Event<'a>>>(
&mut self,
parent_name: N,
event: E,
) -> Result<()> {
let name = parent_name.as_ref();
self.write_event(Event::Start(BytesStart::borrowed(name, name.len())))?;
self.write_event(event)?;
self.write_event(Event::End(BytesEnd::borrowed(name)))?;
Ok(())
#[must_use]
pub fn create_element<'a, N>(&'a mut self, name: &'a N) -> ElementWriter<W>
where
N: 'a + AsRef<[u8]> + ?Sized,
{
ElementWriter {
writer: self,
start_tag: BytesStart::borrowed_name(name.as_ref()),
dralley marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

pub struct ElementWriter<'a, W: Write> {
writer: &'a mut Writer<W>,
start_tag: BytesStart<'a>,
}

impl<'a, W: Write> ElementWriter<'a, W> {
/// Adds an attribute to this element.
pub fn with_attribute<'b, I>(mut self, attr: I) -> Self
where
I: Into<Attribute<'b>>,
{
self.start_tag.push_attribute(attr);
self
}

/// Add additional attributes to this element using an iterator.
///
/// The yielded items must be convertible to [`Attribute`] using `Into`.
///
/// [`Attribute`]: attributes/struct.Attributes.html
pub fn with_attributes<'b, I>(mut self, attributes: I) -> Self
where
I: IntoIterator,
I::Item: Into<Attribute<'b>>,
{
self.start_tag.extend_attributes(attributes);
self
}

/// Write some text inside the current element.
pub fn write_text_content(self, text: BytesText) -> Result<&'a mut Writer<W>> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does a pair of tags with no text always get represented as [Start] [Text ""] [End]?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? If possible I think we could optimize it and write an empty event but this is really not very important.

Copy link
Collaborator Author

@dralley dralley Mar 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to be sure that what I'm doing is correct - if it was ever necessary to write two separate start and end events with no text event (even an empty one) in the middle, this wouldn't be convenient. But if there needs to always be one (unless writing an empty tag, in which case you have better options than this function), then this is fine.

self.writer
.write_event(Event::Start(self.start_tag.to_borrowed()))?;
self.writer.write_event(Event::Text(text))?;
self.writer
.write_event(Event::End(self.start_tag.to_end()))?;
Ok(self.writer)
}

/// Write a CData event `<![CDATA[...]]>` inside the current element.
pub fn write_cdata_content(self, text: BytesText) -> Result<&'a mut Writer<W>> {
self.writer
.write_event(Event::Start(self.start_tag.to_borrowed()))?;
self.writer.write_event(Event::CData(text))?;
self.writer
.write_event(Event::End(self.start_tag.to_end()))?;
Ok(self.writer)
}

/// Write a processing instruction `<?...?>` inside the current element.
pub fn write_pi_content(self, text: BytesText) -> Result<&'a mut Writer<W>> {
self.writer
.write_event(Event::Start(self.start_tag.to_borrowed()))?;
self.writer.write_event(Event::PI(text))?;
self.writer
.write_event(Event::End(self.start_tag.to_end()))?;
Ok(self.writer)
}

/// Write an empty (self-closing) tag.
pub fn write_empty(self) -> Result<&'a mut Writer<W>> {
self.writer.write_event(Event::Empty(self.start_tag))?;
Ok(self.writer)
}

/// Create a new scope for writing XML inside the current element.
pub fn write_inner_content<F>(mut self, closure: F) -> Result<&'a mut Writer<W>>
where
F: Fn(&mut Writer<W>) -> Result<()>,
{
self.writer
.write_event(Event::Start(self.start_tag.to_borrowed()))?;
closure(&mut self.writer)?;
self.writer
.write_event(Event::End(self.start_tag.to_end()))?;
Ok(self.writer)
}
}

Expand Down Expand Up @@ -238,8 +352,8 @@ mod indentation {
.expect("write tag failed");

assert_eq!(
buffer,
br#"<self-closed attr1="value1" attr2="value2"/>"#.as_ref()
std::str::from_utf8(&buffer).unwrap(),
r#"<self-closed attr1="value1" attr2="value2"/>"#
);
}

Expand All @@ -260,10 +374,9 @@ mod indentation {
.expect("write end tag failed");

assert_eq!(
buffer,
br#"<paired attr1="value1" attr2="value2">
std::str::from_utf8(&buffer).unwrap(),
r#"<paired attr1="value1" attr2="value2">
</paired>"#
.as_ref()
);
}

Expand All @@ -289,11 +402,10 @@ mod indentation {
.expect("write end tag failed");

assert_eq!(
buffer,
br#"<paired attr1="value1" attr2="value2">
std::str::from_utf8(&buffer).unwrap(),
r#"<paired attr1="value1" attr2="value2">
<inner/>
</paired>"#
.as_ref()
);
}

Expand All @@ -319,8 +431,8 @@ mod indentation {
.expect("write end tag failed");

assert_eq!(
buffer,
br#"<paired attr1="value1" attr2="value2">text</paired>"#.as_ref()
std::str::from_utf8(&buffer).unwrap(),
r#"<paired attr1="value1" attr2="value2">text</paired>"#
);
}

Expand Down Expand Up @@ -350,10 +462,9 @@ mod indentation {
.expect("write end tag failed");

assert_eq!(
buffer,
br#"<paired attr1="value1" attr2="value2">text<inner/>
std::str::from_utf8(&buffer).unwrap(),
r#"<paired attr1="value1" attr2="value2">text<inner/>
</paired>"#
.as_ref()
);
}

Expand Down Expand Up @@ -385,13 +496,88 @@ mod indentation {
.expect("write end tag 1 failed");

assert_eq!(
buffer,
br#"<paired attr1="value1" attr2="value2">
std::str::from_utf8(&buffer).unwrap(),
r#"<paired attr1="value1" attr2="value2">
<paired attr1="value1" attr2="value2">
<inner/>
</paired>
</paired>"#
.as_ref()
);
}
#[test]
fn element_writer_empty() {
let mut buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut buffer, b' ', 4);

writer
.create_element(b"empty")
.with_attribute(("attr1", "value1"))
.with_attribute(("attr2", "value2"))
dralley marked this conversation as resolved.
Show resolved Hide resolved
.write_empty()
.expect("failure");

assert_eq!(
std::str::from_utf8(&buffer).unwrap(),
r#"<empty attr1="value1" attr2="value2"/>"#
);
}

#[test]
fn element_writer_text() {
let mut buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut buffer, b' ', 4);

writer
.create_element("paired")
.with_attribute(("attr1", "value1"))
.with_attribute(("attr2", "value2"))
.write_text_content(BytesText::from_plain_str("text"))
.expect("failure");

assert_eq!(
std::str::from_utf8(&buffer).unwrap(),
r#"<paired attr1="value1" attr2="value2">text</paired>"#
dralley marked this conversation as resolved.
Show resolved Hide resolved
);
}

#[test]
fn element_writer_nested() {
let mut buffer = Vec::new();
let mut writer = Writer::new_with_indent(&mut buffer, b' ', 4);

writer
.create_element("outer")
.with_attribute(("attr1", "value1"))
.with_attribute(("attr2", "value2"))
.write_inner_content(|writer| {
let fruits = ["apple", "orange", "banana"];
for (quant, item) in fruits.iter().enumerate() {
writer
.create_element("fruit")
.with_attribute(("quantity", quant.to_string().as_str()))
.write_text_content(BytesText::from_plain_str(item))?;
}
writer
.create_element("inner")
.write_inner_content(|writer| {
writer.create_element("empty").write_empty()?;
Ok(())
})?;

Ok(())
})
.expect("failure");

assert_eq!(
std::str::from_utf8(&buffer).unwrap(),
r#"<outer attr1="value1" attr2="value2">
<fruit quantity="0">apple</fruit>
<fruit quantity="1">orange</fruit>
<fruit quantity="2">banana</fruit>
<inner>
<empty/>
</inner>
</outer>"#
);
}
}