fastx-io 0.3.0

Fast, streaming FASTA/FASTQ reader and writer for bioinformatics pipelines
Documentation
//! Fast, streaming FASTA/FASTQ I/O for bioinformatics pipelines.
//!
//! `fastx` covers the everyday sequence work that would otherwise pull in
//! BioPython or BioPerl: reading and writing FASTA/FASTQ (plain or gzipped),
//! reverse complementing, translating, quality trimming, assembly statistics and
//! `samtools faidx`-style random access — with a streaming parser that keeps
//! memory flat regardless of file size.
//!
//! # Reading
//!
//! ```no_run
//! // Format and gzip are detected from the extension and the magic bytes.
//! for record in fastx::open("reads.fq.gz")? {
//!     let record = record?;
//!     println!("{}\t{}\t{:?}", record.id, record.len(), record.gc_content());
//! }
//! # Ok::<(), fastx::Error>(())
//! ```
//!
//! In a hot loop, reuse one record so parsing does not allocate at all:
//!
//! ```
//! use fastx::{FastxReader, Sequence};
//!
//! let data = b"@r1\nACGTN\n+\nIIIII\n";
//! let mut reader = FastxReader::new(&data[..]);
//! let mut record = Sequence::default();
//! while reader.read_into(&mut record)? {
//!     assert_eq!(record.len(), 5);
//! }
//! # Ok::<(), fastx::Error>(())
//! ```
//!
//! For a pass that never keeps a record, [`FastxReader::read_ref`] borrows
//! straight from the read buffer and copies nothing at all:
//!
//! ```
//! use fastx::FastxReader;
//!
//! let data = b"@r1\nACGTN\n+\nIIIII\n";
//! let mut bases = 0;
//! FastxReader::new(&data[..]).for_each_ref(|record| {
//!     bases += record.len();
//!     Ok(())
//! })?;
//! assert_eq!(bases, 5);
//! # Ok::<(), fastx::Error>(())
//! ```
//!
//! # Writing
//!
//! ```no_run
//! use fastx::{Alphabet, Sequence, WriterBuilder};
//!
//! let mut writer = WriterBuilder::new()
//!     .line_width(80)            // FASTA wrapping; 0 disables it
//!     .validate(Alphabet::Dna)   // reject anything but ACGTN before writing
//!     .create("contigs.fa.gz")?; // gzip inferred from the extension
//!
//! writer.write_record(&Sequence::fasta("contig1", b"ACGTACGT"))?;
//! writer.finish()?;              // always finish: it surfaces gzip errors
//! # Ok::<(), fastx::Error>(())
//! ```
//!
//! # Converting FASTQ to FASTA
//!
//! ```
//! use fastx::{FastxReader, FastxWriter, Format};
//!
//! let fastq = b"@r1 sample\nACGTACGT\n+\nIIIIIIII\n";
//! let mut out = Vec::new();
//! let mut writer = FastxWriter::new(&mut out, Format::Fasta).line_width(4);
//! for record in FastxReader::new(&fastq[..]) {
//!     writer.write_record(&record?)?;
//! }
//! writer.flush()?;
//! assert_eq!(out, b">r1 sample\nACGT\nACGT\n");
//! # Ok::<(), fastx::Error>(())
//! ```
//!
//! # Cargo features
//!
//! | feature | default | effect |
//! |---|---|---|
//! | `gzip` | yes | transparent gzip/BGZF reading and writing via `flate2` |
//! | `parallel` | no | [`parallel`] module: batched and chunk-split multicore processing via `rayon` |
//! | `zstd` | no | Zstandard reading and writing; needs a C toolchain and Rust 1.85 |
//! | `libdeflate` | no | swaps BGZF's deflate for libdeflate: ~2× faster and slightly smaller. Same C toolchain and Rust 1.85 |
//! | `full` | no | everything above |
//!
//! The crate's own MSRV is 1.74, but a feature inherits the MSRV of what it
//! pulls in: `parallel` needs 1.80, and `zstd` and `libdeflate` need 1.85.
//!
//! # Design notes
//!
//! * **Streaming by default.** [`FastxReader`] holds one growable buffer and one
//!   record; a 300 GB FASTQ uses the same memory as a 300 byte one. Lines longer
//!   than the buffer grow it, so single-line chromosomes work too.
//! * **Multi-line records.** Wrapped FASTA is joined transparently, and
//!   multi-line FASTQ is handled by matching quality length to sequence length
//!   rather than by assuming four lines per record — which also makes `@` at the
//!   start of a quality line harmless.
//! * **Errors carry line numbers.** Malformed input produces [`Error::Parse`]
//!   with the offending line, not a silent skip.
//! * **No silent data invention.** Writing FASTQ without quality scores is an
//!   error; writing a FASTQ record as FASTA drops quality, which is what
//!   `fq2fa` should do.

#![deny(missing_docs)]
#![warn(clippy::all)]
#![forbid(unsafe_code)]

#[cfg(feature = "gzip")]
pub mod bgzf;
pub mod borrowed;
pub mod error;
pub mod format;
pub mod index;
pub mod paired;
pub mod qual;
pub mod reader;
pub mod record;
pub mod seq;
pub mod stats;
pub mod writer;

#[cfg(feature = "parallel")]
pub mod parallel;

pub use crate::borrowed::SequenceRef;
pub use crate::error::{Error, ParseError, Result};
pub use crate::format::{Compression, CompressionLevel, Format};
pub use crate::index::{FastaIndex, IndexedFasta};
pub use crate::paired::{Pair, PairedReader, PairedWriter};
pub use crate::qual::QualityEncoding;
pub use crate::reader::{from_stdin, open, BoxedReader, FastxReader, ReaderBuilder, Records};
pub use crate::record::Sequence;
pub use crate::seq::{Alphabet, BaseCounts};
pub use crate::stats::SeqStats;
pub use crate::writer::{create, BoxedWriter, FastxWriter, WriterBuilder};

/// Everything you normally need, in one `use`.
///
/// ```
/// use fastx::prelude::*;
///
/// let mut stats = SeqStats::new();
/// FastxReader::new(&b">a\nACGT\n"[..]).for_each_record(|r| { stats.push(r); Ok(()) })?;
/// assert_eq!(stats.count, 1);
/// # Ok::<(), fastx::Error>(())
/// ```
pub mod prelude {
    pub use crate::error::{Error as FastxError, Result as FastxResult};
    pub use crate::format::Format;
    pub use crate::reader::FastxReader;
    pub use crate::record::Sequence;
    pub use crate::seq::Alphabet;
    pub use crate::stats::SeqStats;
    pub use crate::writer::FastxWriter;
}

/// Read every record of a file into memory.
///
/// Convenient for reference files and test fixtures; use [`open`] for anything
/// that might not fit in RAM.
///
/// ```no_run
/// let records = fastx::read_all("primers.fa")?;
/// assert!(!records.is_empty());
/// # Ok::<(), fastx::Error>(())
/// ```
pub fn read_all<P: AsRef<std::path::Path>>(path: P) -> Result<Vec<Sequence>> {
    open(path)?.collect()
}

/// Read every record from any reader into memory.
///
/// ```
/// let records = fastx::read_all_from(&b">a\nACGT\n>b\nTT\n"[..])?;
/// assert_eq!(records.len(), 2);
/// # Ok::<(), fastx::Error>(())
/// ```
pub fn read_all_from<R: std::io::Read>(reader: R) -> Result<Vec<Sequence>> {
    FastxReader::new(reader).collect()
}

/// Write records to a file, inferring format and compression from the path.
///
/// ```no_run
/// let records = vec![fastx::Sequence::fasta("a", b"ACGT")];
/// fastx::write_all("out.fa", &records)?;
/// # Ok::<(), fastx::Error>(())
/// ```
pub fn write_all<'a, P, I>(path: P, records: I) -> Result<()>
where
    P: AsRef<std::path::Path>,
    I: IntoIterator<Item = &'a Sequence>,
{
    let mut writer = create(path)?;
    writer.write_all(records)?;
    writer.finish()?;
    Ok(())
}

/// Summary statistics for a file, in one call.
///
/// ```no_run
/// let stats = fastx::stats_of("reads.fq.gz")?;
/// println!("{stats}");
/// # Ok::<(), fastx::Error>(())
/// ```
pub fn stats_of<P: AsRef<std::path::Path>>(path: P) -> Result<SeqStats> {
    let mut stats = SeqStats::new();
    open(path)?.for_each_record(|record| {
        stats.push(record);
        Ok(())
    })?;
    Ok(stats)
}