Skip to main content

fastx/
lib.rs

1//! Fast, streaming FASTA/FASTQ I/O for bioinformatics pipelines.
2//!
3//! `fastx` covers the everyday sequence work that would otherwise pull in
4//! BioPython or BioPerl: reading and writing FASTA/FASTQ (plain or gzipped),
5//! reverse complementing, translating, quality trimming, assembly statistics and
6//! `samtools faidx`-style random access — with a streaming parser that keeps
7//! memory flat regardless of file size.
8//!
9//! # Reading
10//!
11//! ```no_run
12//! // Format and gzip are detected from the extension and the magic bytes.
13//! for record in fastx::open("reads.fq.gz")? {
14//!     let record = record?;
15//!     println!("{}\t{}\t{:?}", record.id, record.len(), record.gc_content());
16//! }
17//! # Ok::<(), fastx::Error>(())
18//! ```
19//!
20//! In a hot loop, reuse one record so parsing does not allocate at all:
21//!
22//! ```
23//! use fastx::{FastxReader, Sequence};
24//!
25//! let data = b"@r1\nACGTN\n+\nIIIII\n";
26//! let mut reader = FastxReader::new(&data[..]);
27//! let mut record = Sequence::default();
28//! while reader.read_into(&mut record)? {
29//!     assert_eq!(record.len(), 5);
30//! }
31//! # Ok::<(), fastx::Error>(())
32//! ```
33//!
34//! # Writing
35//!
36//! ```no_run
37//! use fastx::{Alphabet, Sequence, WriterBuilder};
38//!
39//! let mut writer = WriterBuilder::new()
40//!     .line_width(80)            // FASTA wrapping; 0 disables it
41//!     .validate(Alphabet::Dna)   // reject anything but ACGTN before writing
42//!     .create("contigs.fa.gz")?; // gzip inferred from the extension
43//!
44//! writer.write_record(&Sequence::fasta("contig1", b"ACGTACGT"))?;
45//! writer.finish()?;              // always finish: it surfaces gzip errors
46//! # Ok::<(), fastx::Error>(())
47//! ```
48//!
49//! # Converting FASTQ to FASTA
50//!
51//! ```
52//! use fastx::{FastxReader, FastxWriter, Format};
53//!
54//! let fastq = b"@r1 sample\nACGTACGT\n+\nIIIIIIII\n";
55//! let mut out = Vec::new();
56//! let mut writer = FastxWriter::new(&mut out, Format::Fasta).line_width(4);
57//! for record in FastxReader::new(&fastq[..]) {
58//!     writer.write_record(&record?)?;
59//! }
60//! writer.flush()?;
61//! assert_eq!(out, b">r1 sample\nACGT\nACGT\n");
62//! # Ok::<(), fastx::Error>(())
63//! ```
64//!
65//! # Cargo features
66//!
67//! | feature | default | effect |
68//! |---|---|---|
69//! | `gzip` | yes | transparent gzip/BGZF reading and writing via `flate2` |
70//! | `parallel` | no | [`parallel`] module: batched and chunk-split multicore processing via `rayon` |
71//! | `zstd` | no | Zstandard reading and writing; needs a C toolchain and Rust 1.85 |
72//! | `full` | no | everything above |
73//!
74//! The crate's own MSRV is 1.74, but a feature inherits the MSRV of what it
75//! pulls in: `parallel` needs 1.80 and `zstd` needs 1.85.
76//!
77//! # Design notes
78//!
79//! * **Streaming by default.** [`FastxReader`] holds one growable buffer and one
80//!   record; a 300 GB FASTQ uses the same memory as a 300 byte one. Lines longer
81//!   than the buffer grow it, so single-line chromosomes work too.
82//! * **Multi-line records.** Wrapped FASTA is joined transparently, and
83//!   multi-line FASTQ is handled by matching quality length to sequence length
84//!   rather than by assuming four lines per record — which also makes `@` at the
85//!   start of a quality line harmless.
86//! * **Errors carry line numbers.** Malformed input produces [`Error::Parse`]
87//!   with the offending line, not a silent skip.
88//! * **No silent data invention.** Writing FASTQ without quality scores is an
89//!   error; writing a FASTQ record as FASTA drops quality, which is what
90//!   `fq2fa` should do.
91
92#![deny(missing_docs)]
93#![warn(clippy::all)]
94#![forbid(unsafe_code)]
95
96#[cfg(feature = "gzip")]
97pub mod bgzf;
98pub mod error;
99pub mod format;
100pub mod index;
101pub mod paired;
102pub mod qual;
103pub mod reader;
104pub mod record;
105pub mod seq;
106pub mod stats;
107pub mod writer;
108
109#[cfg(feature = "parallel")]
110pub mod parallel;
111
112pub use crate::error::{Error, ParseError, Result};
113pub use crate::format::{Compression, CompressionLevel, Format};
114pub use crate::index::{FastaIndex, IndexedFasta};
115pub use crate::paired::{Pair, PairedReader, PairedWriter};
116pub use crate::qual::QualityEncoding;
117pub use crate::reader::{from_stdin, open, BoxedReader, FastxReader, ReaderBuilder, Records};
118pub use crate::record::Sequence;
119pub use crate::seq::{Alphabet, BaseCounts};
120pub use crate::stats::SeqStats;
121pub use crate::writer::{create, BoxedWriter, FastxWriter, WriterBuilder};
122
123/// Everything you normally need, in one `use`.
124///
125/// ```
126/// use fastx::prelude::*;
127///
128/// let mut stats = SeqStats::new();
129/// FastxReader::new(&b">a\nACGT\n"[..]).for_each_record(|r| { stats.push(r); Ok(()) })?;
130/// assert_eq!(stats.count, 1);
131/// # Ok::<(), fastx::Error>(())
132/// ```
133pub mod prelude {
134    pub use crate::error::{Error as FastxError, Result as FastxResult};
135    pub use crate::format::Format;
136    pub use crate::reader::FastxReader;
137    pub use crate::record::Sequence;
138    pub use crate::seq::Alphabet;
139    pub use crate::stats::SeqStats;
140    pub use crate::writer::FastxWriter;
141}
142
143/// Read every record of a file into memory.
144///
145/// Convenient for reference files and test fixtures; use [`open`] for anything
146/// that might not fit in RAM.
147///
148/// ```no_run
149/// let records = fastx::read_all("primers.fa")?;
150/// assert!(!records.is_empty());
151/// # Ok::<(), fastx::Error>(())
152/// ```
153pub fn read_all<P: AsRef<std::path::Path>>(path: P) -> Result<Vec<Sequence>> {
154    open(path)?.collect()
155}
156
157/// Read every record from any reader into memory.
158///
159/// ```
160/// let records = fastx::read_all_from(&b">a\nACGT\n>b\nTT\n"[..])?;
161/// assert_eq!(records.len(), 2);
162/// # Ok::<(), fastx::Error>(())
163/// ```
164pub fn read_all_from<R: std::io::Read>(reader: R) -> Result<Vec<Sequence>> {
165    FastxReader::new(reader).collect()
166}
167
168/// Write records to a file, inferring format and compression from the path.
169///
170/// ```no_run
171/// let records = vec![fastx::Sequence::fasta("a", b"ACGT")];
172/// fastx::write_all("out.fa", &records)?;
173/// # Ok::<(), fastx::Error>(())
174/// ```
175pub fn write_all<'a, P, I>(path: P, records: I) -> Result<()>
176where
177    P: AsRef<std::path::Path>,
178    I: IntoIterator<Item = &'a Sequence>,
179{
180    let mut writer = create(path)?;
181    writer.write_all(records)?;
182    writer.finish()?;
183    Ok(())
184}
185
186/// Summary statistics for a file, in one call.
187///
188/// ```no_run
189/// let stats = fastx::stats_of("reads.fq.gz")?;
190/// println!("{stats}");
191/// # Ok::<(), fastx::Error>(())
192/// ```
193pub fn stats_of<P: AsRef<std::path::Path>>(path: P) -> Result<SeqStats> {
194    let mut stats = SeqStats::new();
195    open(path)?.for_each_record(|record| {
196        stats.push(record);
197        Ok(())
198    })?;
199    Ok(stats)
200}