Skip to content

Commit

Permalink
Merge pull request #573 from vilunov/async-writer
Browse files Browse the repository at this point in the history
Implement basic async writer
  • Loading branch information
Mingun committed Mar 11, 2023
2 parents 7792983 + 5adef14 commit f822669
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 11 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [#569]: Rewrite the `Reader::read_event_into_async` as an async fn, making the future `Send` if possible.
- [#571]: Borrow element names (`<element>`) when deserialize with serde.
This change allow to deserialize into `HashMap<&str, T>`, for example
- [#573]: Add basic support for async byte writers via tokio's `AsyncWrite`.

### Bug Fixes

Expand Down Expand Up @@ -58,6 +59,7 @@
[#568]: https://github.com/tafia/quick-xml/pull/568
[#569]: https://github.com/tafia/quick-xml/pull/569
[#571]: https://github.com/tafia/quick-xml/pull/571
[#573]: https://github.com/tafia/quick-xml/pull/573

## 0.27.1 -- 2022-12-28

Expand Down
27 changes: 16 additions & 11 deletions src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use crate::encoding::UTF8_BOM;
use crate::errors::Result;
use crate::events::{attributes::Attribute, BytesCData, BytesStart, BytesText, Event};

/// XML writer. Writes XML [`Event`]s to a [`std::io::Write`] implementor.
#[cfg(feature = "async-tokio")]
mod async_tokio;

/// XML writer. Writes XML [`Event`]s to a [`std::io::Write`] or [`tokio::io::AsyncWrite`] implementor.
///
/// # Examples
///
Expand Down Expand Up @@ -53,13 +56,13 @@ use crate::events::{attributes::Attribute, BytesCData, BytesStart, BytesText, Ev
/// assert_eq!(result, expected.as_bytes());
/// ```
#[derive(Clone)]
pub struct Writer<W: Write> {
pub struct Writer<W> {
/// underlying writer
writer: W,
indent: Option<Indentation>,
}

impl<W: Write> Writer<W> {
impl<W> Writer<W> {
/// Creates a `Writer` from a generic writer.
pub fn new(inner: W) -> Writer<W> {
Writer {
Expand All @@ -68,14 +71,6 @@ impl<W: Write> Writer<W> {
}
}

/// Creates a `Writer` with configured whitespace indents from a generic writer.
pub fn new_with_indent(inner: W, indent_char: u8, indent_size: usize) -> Writer<W> {
Writer {
writer: inner,
indent: Some(Indentation::new(indent_char, indent_size)),
}
}

/// Consumes this `Writer`, returning the underlying writer.
pub fn into_inner(self) -> W {
self.writer
Expand All @@ -90,6 +85,16 @@ impl<W: Write> Writer<W> {
pub fn get_ref(&self) -> &W {
&self.writer
}
}

impl<W: Write> Writer<W> {
/// Creates a `Writer` with configured whitespace indents from a generic writer.
pub fn new_with_indent(inner: W, indent_char: u8, indent_size: usize) -> Writer<W> {
Writer {
writer: inner,
indent: Some(Indentation::new(indent_char, indent_size)),
}
}

/// Write a [Byte-Order-Mark] character to the document.
///
Expand Down
119 changes: 119 additions & 0 deletions src/writer/async_tokio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use tokio::io::{AsyncWrite, AsyncWriteExt};

use crate::errors::Result;
use crate::events::Event;
use crate::Writer;

impl<W: AsyncWrite + Unpin> Writer<W> {
/// Writes the given event to the underlying writer. Async version of [`Writer::write_event`].
pub async fn write_event_async<'a, E: AsRef<Event<'a>>>(&mut self, event: E) -> Result<()> {
match *event.as_ref() {
Event::Start(ref e) => self.write_wrapped_async(b"<", e, b">").await,
Event::End(ref e) => self.write_wrapped_async(b"</", e, b">").await,
Event::Empty(ref e) => self.write_wrapped_async(b"<", e, b"/>").await,
Event::Text(ref e) => self.write_async(e).await,
Event::Comment(ref e) => self.write_wrapped_async(b"<!--", e, b"-->").await,
Event::CData(ref e) => {
self.write_async(b"<![CDATA[").await?;
self.write_async(e).await?;
self.write_async(b"]]>").await
}
Event::Decl(ref e) => self.write_wrapped_async(b"<?", e, b"?>").await,
Event::PI(ref e) => self.write_wrapped_async(b"<?", e, b"?>").await,
Event::DocType(ref e) => self.write_wrapped_async(b"<!DOCTYPE ", e, b">").await,
Event::Eof => Ok(()),
}
}

#[inline]
async fn write_async(&mut self, value: &[u8]) -> Result<()> {
self.writer.write_all(value).await.map_err(Into::into)
}

#[inline]
async fn write_wrapped_async(
&mut self,
before: &[u8],
value: &[u8],
after: &[u8],
) -> Result<()> {
self.write_async(before).await?;
self.write_async(value).await?;
self.write_async(after).await?;
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::events::*;
use pretty_assertions::assert_eq;

macro_rules! test {
($name: ident, $event: expr, $expected: expr) => {
#[tokio::test]
async fn $name() {
let mut buffer = Vec::new();
let mut writer = Writer::new(&mut buffer);

writer
.write_event_async($event)
.await
.expect("write event failed");

assert_eq!(std::str::from_utf8(&buffer).unwrap(), $expected,);
}
};
}

test!(
xml_header,
Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), Some("no"))),
r#"<?xml version="1.0" encoding="UTF-8" standalone="no"?>"#
);

test!(empty_tag, Event::Empty(BytesStart::new("tag")), r#"<tag/>"#);

test!(
comment,
Event::Comment(BytesText::new("this is a comment")),
r#"<!--this is a comment-->"#
);

test!(
cdata,
Event::CData(BytesCData::new("this is a cdata")),
r#"<![CDATA[this is a cdata]]>"#
);

test!(
pi,
Event::PI(BytesText::new("this is a processing instruction")),
r#"<?this is a processing instruction?>"#
);

test!(
doctype,
Event::DocType(BytesText::new("this is a doctype")),
r#"<!DOCTYPE this is a doctype>"#
);

#[tokio::test]
async fn full_tag() {
let mut buffer = Vec::new();
let mut writer = Writer::new(&mut buffer);

let start = Event::Start(BytesStart::new("tag"));
let text = Event::Text(BytesText::new("inner text"));
let end = Event::End(BytesEnd::new("tag"));
for i in [start, text, end] {
writer.write_event_async(i).await.expect("write tag failed");
}

assert_eq!(
std::str::from_utf8(&buffer).unwrap(),
r#"<tag>inner text</tag>"#
);
}
}

0 comments on commit f822669

Please sign in to comment.