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