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//! | `full` | no | everything above |
72//!
73//! # Design notes
74//!
75//! * **Streaming by default.** [`FastxReader`] holds one growable buffer and one
76//! record; a 300 GB FASTQ uses the same memory as a 300 byte one. Lines longer
77//! than the buffer grow it, so single-line chromosomes work too.
78//! * **Multi-line records.** Wrapped FASTA is joined transparently, and
79//! multi-line FASTQ is handled by matching quality length to sequence length
80//! rather than by assuming four lines per record — which also makes `@` at the
81//! start of a quality line harmless.
82//! * **Errors carry line numbers.** Malformed input produces [`Error::Parse`]
83//! with the offending line, not a silent skip.
84//! * **No silent data invention.** Writing FASTQ without quality scores is an
85//! error; writing a FASTQ record as FASTA drops quality, which is what
86//! `fq2fa` should do.
87
88#![deny(missing_docs)]
89#![warn(clippy::all)]
90#![forbid(unsafe_code)]
91
92#[cfg(feature = "gzip")]
93pub mod bgzf;
94pub mod error;
95pub mod format;
96pub mod index;
97pub mod paired;
98pub mod qual;
99pub mod reader;
100pub mod record;
101pub mod seq;
102pub mod stats;
103pub mod writer;
104
105#[cfg(feature = "parallel")]
106pub mod parallel;
107
108pub use crate::error::{Error, ParseError, Result};
109pub use crate::format::{Compression, CompressionLevel, Format};
110pub use crate::index::{FastaIndex, IndexedFasta};
111pub use crate::paired::{Pair, PairedReader, PairedWriter};
112pub use crate::qual::QualityEncoding;
113pub use crate::reader::{from_stdin, open, BoxedReader, FastxReader, ReaderBuilder, Records};
114pub use crate::record::Sequence;
115pub use crate::seq::{Alphabet, BaseCounts};
116pub use crate::stats::SeqStats;
117pub use crate::writer::{create, BoxedWriter, FastxWriter, WriterBuilder};
118
119/// Everything you normally need, in one `use`.
120///
121/// ```
122/// use fastx::prelude::*;
123///
124/// let mut stats = SeqStats::new();
125/// FastxReader::new(&b">a\nACGT\n"[..]).for_each_record(|r| { stats.push(r); Ok(()) })?;
126/// assert_eq!(stats.count, 1);
127/// # Ok::<(), fastx::Error>(())
128/// ```
129pub mod prelude {
130 pub use crate::error::{Error as FastxError, Result as FastxResult};
131 pub use crate::format::Format;
132 pub use crate::reader::FastxReader;
133 pub use crate::record::Sequence;
134 pub use crate::seq::Alphabet;
135 pub use crate::stats::SeqStats;
136 pub use crate::writer::FastxWriter;
137}
138
139/// Read every record of a file into memory.
140///
141/// Convenient for reference files and test fixtures; use [`open`] for anything
142/// that might not fit in RAM.
143///
144/// ```no_run
145/// let records = fastx::read_all("primers.fa")?;
146/// assert!(!records.is_empty());
147/// # Ok::<(), fastx::Error>(())
148/// ```
149pub fn read_all<P: AsRef<std::path::Path>>(path: P) -> Result<Vec<Sequence>> {
150 open(path)?.collect()
151}
152
153/// Read every record from any reader into memory.
154///
155/// ```
156/// let records = fastx::read_all_from(&b">a\nACGT\n>b\nTT\n"[..])?;
157/// assert_eq!(records.len(), 2);
158/// # Ok::<(), fastx::Error>(())
159/// ```
160pub fn read_all_from<R: std::io::Read>(reader: R) -> Result<Vec<Sequence>> {
161 FastxReader::new(reader).collect()
162}
163
164/// Write records to a file, inferring format and compression from the path.
165///
166/// ```no_run
167/// let records = vec![fastx::Sequence::fasta("a", b"ACGT")];
168/// fastx::write_all("out.fa", &records)?;
169/// # Ok::<(), fastx::Error>(())
170/// ```
171pub fn write_all<'a, P, I>(path: P, records: I) -> Result<()>
172where
173 P: AsRef<std::path::Path>,
174 I: IntoIterator<Item = &'a Sequence>,
175{
176 let mut writer = create(path)?;
177 writer.write_all(records)?;
178 writer.finish()?;
179 Ok(())
180}
181
182/// Summary statistics for a file, in one call.
183///
184/// ```no_run
185/// let stats = fastx::stats_of("reads.fq.gz")?;
186/// println!("{stats}");
187/// # Ok::<(), fastx::Error>(())
188/// ```
189pub fn stats_of<P: AsRef<std::path::Path>>(path: P) -> Result<SeqStats> {
190 let mut stats = SeqStats::new();
191 open(path)?.for_each_record(|record| {
192 stats.push(record);
193 Ok(())
194 })?;
195 Ok(stats)
196}