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

Implement basic async writer #573

Merged
merged 5 commits into from
Mar 11, 2023
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
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>"#
);
}
}