diff --git a/noodles-fasta/CHANGELOG.md b/noodles-fasta/CHANGELOG.md index de0c794a9..23e3c53d4 100644 --- a/noodles-fasta/CHANGELOG.md +++ b/noodles-fasta/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + + * fasta/async/io: Add async writer (`fasta::r#async::io::Writer`). + ### Deprecated * fasta: Deprecate async re-export (`AsyncReader`). diff --git a/noodles-fasta/src/async/io.rs b/noodles-fasta/src/async/io.rs index 6afbd1d00..2eb256444 100644 --- a/noodles-fasta/src/async/io.rs +++ b/noodles-fasta/src/async/io.rs @@ -1,5 +1,6 @@ //! Async FASTA I/O. pub(crate) mod reader; +mod writer; -pub use self::reader::Reader; +pub use self::{reader::Reader, writer::Writer}; diff --git a/noodles-fasta/src/async/io/writer.rs b/noodles-fasta/src/async/io/writer.rs new file mode 100644 index 000000000..0f42de674 --- /dev/null +++ b/noodles-fasta/src/async/io/writer.rs @@ -0,0 +1,101 @@ +//! Async FASTA writer. + +mod record; + +use tokio::io::{self, AsyncWrite}; + +use self::record::write_record; +use crate::Record; + +/// An async FASTA writer. +pub struct Writer { + inner: W, +} + +impl Writer { + /// Returns a reference to the underlying writer. + /// + /// # Examples + /// + /// ``` + /// use noodles_fasta as fasta; + /// use tokio::io; + /// let writer = fasta::r#async::io::Writer::new(io::sink()); + /// let _inner = writer.get_ref(); + /// ``` + pub fn get_ref(&self) -> &W { + &self.inner + } + + /// Returns a mutable reference to the underlying writer. + /// + /// # Examples + /// + /// ``` + /// use noodles_fasta as fasta; + /// use tokio::io; + /// let mut writer = fasta::r#async::io::Writer::new(io::sink()); + /// let _inner = writer.get_mut(); + /// ``` + pub fn get_mut(&mut self) -> &mut W { + &mut self.inner + } + + /// Unwraps and returns the underlying writer. + /// + /// # Examples + /// + /// ``` + /// use noodles_fasta as fasta; + /// use tokio::io; + /// let writer = fasta::r#async::io::Writer::new(io::sink()); + /// let _inner = writer.into_inner(); + /// ``` + pub fn into_inner(self) -> W { + self.inner + } +} + +impl Writer +where + W: AsyncWrite + Unpin, +{ + /// Creates a FASTA writer. + /// + /// # Examples + /// + /// ``` + /// use noodles_fasta as fasta; + /// use tokio::io; + /// let writer = fasta::r#async::io::Writer::new(io::sink()); + /// ``` + pub fn new(inner: W) -> Self { + Self { inner } + } + + /// Writes a FASTA record. + /// + /// Sequence lines are hard wrapped at 80 bases. + /// + /// # Examples + /// + /// ``` + /// # #[tokio::main] + /// # async fn main() -> tokio::io::Result<()> { + /// use noodles_fasta::{self as fasta, record::{Definition, Sequence}}; + /// use tokio::io; + /// + /// let mut writer = fasta::r#async::io::Writer::new(io::sink()); + /// + /// let definition = Definition::new("sq0", None); + /// let sequence = Sequence::from(b"ACGT".to_vec()); + /// let record = fasta::Record::new(definition, sequence); + /// + /// writer.write_record(&record).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn write_record(&mut self, record: &Record) -> io::Result<()> { + write_record(&mut self.inner, record).await + } +} diff --git a/noodles-fasta/src/async/io/writer/record.rs b/noodles-fasta/src/async/io/writer/record.rs new file mode 100644 index 000000000..a17c6e56f --- /dev/null +++ b/noodles-fasta/src/async/io/writer/record.rs @@ -0,0 +1,29 @@ +mod definition; +mod sequence; + +use tokio::io::{self, AsyncWrite, AsyncWriteExt}; + +use self::{definition::write_definition, sequence::write_sequence}; +use crate::Record; + +const LINE_BASES: usize = 80; + +pub(super) async fn write_record(writer: &mut W, record: &Record) -> io::Result<()> +where + W: AsyncWrite + Unpin, +{ + write_definition(writer, record.definition()).await?; + write_newline(writer).await?; + + write_sequence(writer, record.sequence(), LINE_BASES).await?; + + Ok(()) +} + +async fn write_newline(writer: &mut W) -> io::Result<()> +where + W: AsyncWrite + Unpin, +{ + const LINE_FEED: u8 = b'\n'; + writer.write_all(&[LINE_FEED]).await +} diff --git a/noodles-fasta/src/async/io/writer/record/definition.rs b/noodles-fasta/src/async/io/writer/record/definition.rs new file mode 100644 index 000000000..c4dbcf1c1 --- /dev/null +++ b/noodles-fasta/src/async/io/writer/record/definition.rs @@ -0,0 +1,75 @@ +use tokio::io::{self, AsyncWrite, AsyncWriteExt}; + +use crate::record::Definition; + +pub(super) async fn write_definition(writer: &mut W, definition: &Definition) -> io::Result<()> +where + W: AsyncWrite + Unpin, +{ + write_prefix(writer).await?; + write_name(writer, definition.name()).await?; + + if let Some(description) = definition.description() { + write_separator(writer).await?; + write_description(writer, description).await?; + } + + Ok(()) +} + +async fn write_prefix(writer: &mut W) -> io::Result<()> +where + W: AsyncWrite + Unpin, +{ + const PREFIX: u8 = b'>'; + writer.write_all(&[PREFIX]).await +} + +async fn write_name(writer: &mut W, name: &[u8]) -> io::Result<()> +where + W: AsyncWrite + Unpin, +{ + writer.write_all(name).await +} + +async fn write_separator(writer: &mut W) -> io::Result<()> +where + W: AsyncWrite + Unpin, +{ + const SEPARATOR: u8 = b' '; + writer.write_all(&[SEPARATOR]).await +} + +async fn write_description(writer: &mut W, description: &[u8]) -> io::Result<()> +where + W: AsyncWrite + Unpin, +{ + writer.write_all(description).await +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_write_definition() -> io::Result<()> { + async fn t(buf: &mut Vec, definition: &Definition, expected: &[u8]) -> io::Result<()> { + buf.clear(); + write_definition(buf, definition).await?; + assert_eq!(buf, expected); + Ok(()) + } + + let mut buf = Vec::new(); + + t(&mut buf, &Definition::new("sq0", None), b">sq0").await?; + t( + &mut buf, + &Definition::new("sq0", Some(Vec::from("LN:8"))), + b">sq0 LN:8", + ) + .await?; + + Ok(()) + } +} diff --git a/noodles-fasta/src/async/io/writer/record/sequence.rs b/noodles-fasta/src/async/io/writer/record/sequence.rs new file mode 100644 index 000000000..727b7917e --- /dev/null +++ b/noodles-fasta/src/async/io/writer/record/sequence.rs @@ -0,0 +1,50 @@ +use tokio::io::{self, AsyncWrite, AsyncWriteExt}; + +use super::write_newline; +use crate::record::Sequence; + +pub(super) async fn write_sequence( + writer: &mut W, + sequence: &Sequence, + line_bases: usize, +) -> io::Result<()> +where + W: AsyncWrite + Unpin, +{ + for bases in sequence.as_ref().chunks(line_bases) { + writer.write_all(bases).await?; + write_newline(writer).await?; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_write_sequence() -> io::Result<()> { + let mut writer = Vec::new(); + let sequence = Sequence::from(b"AC".to_vec()); + write_sequence(&mut writer, &sequence, 4).await?; + assert_eq!(writer, b"AC\n"); + + writer.clear(); + let sequence = Sequence::from(b"ACGT".to_vec()); + write_sequence(&mut writer, &sequence, 4).await?; + assert_eq!(writer, b"ACGT\n"); + + writer.clear(); + let sequence = Sequence::from(b"ACGTACGT".to_vec()); + write_sequence(&mut writer, &sequence, 4).await?; + assert_eq!(writer, b"ACGT\nACGT\n"); + + writer.clear(); + let sequence = Sequence::from(b"ACGTACGTAC".to_vec()); + write_sequence(&mut writer, &sequence, 4).await?; + assert_eq!(writer, b"ACGT\nACGT\nAC\n"); + + Ok(()) + } +}