-
Notifications
You must be signed in to change notification settings - Fork 244
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
|
@@ -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}; | ||
|
@@ -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>> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} | ||
|
||
|
@@ -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"/>"# | ||
); | ||
} | ||
|
||
|
@@ -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() | ||
); | ||
} | ||
|
||
|
@@ -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() | ||
); | ||
} | ||
|
||
|
@@ -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>"# | ||
); | ||
} | ||
|
||
|
@@ -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() | ||
); | ||
} | ||
|
||
|
@@ -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>"# | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
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]
, orVec<u8>
.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?