use crate::{
DEFAULT_SIMD_LANES,
data::{
err::ResultWithErrorContext,
id_types::FastaIDs,
types::{
amino_acids::AminoAcids,
nucleotides::{self, Nucleotides, ToDNA, Translate},
},
validation::CheckSequence,
vec_types::{ChopLineBreak, StripLineBreak},
},
search::ByteSplitIter,
unwrap_or_return_some_err,
};
use std::{
fs::File,
io::{BufRead, BufReader, Error as IOError, ErrorKind},
path::Path,
};
#[cfg(feature = "dev-generic-fasta")]
pub mod generic;
#[cfg(test)]
mod test;
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct FastaSeq {
pub name: String,
pub sequence: Vec<u8>,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct FastaNT {
pub name: String,
pub sequence: Nucleotides,
}
#[derive(Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct FastaAA {
pub name: String,
pub sequence: AminoAcids,
}
#[derive(Debug)]
pub struct FastaReader<R: std::io::Read> {
reader: std::io::BufReader<R>,
buffer: Vec<u8>,
first_record: bool,
}
impl FastaSeq {
pub fn reverse_complement(&mut self) {
self.sequence = nucleotides::reverse_complement(&self.sequence);
}
#[inline]
#[must_use]
pub fn recode_to_dna(self) -> FastaNT {
FastaNT {
name: self.name,
sequence: self.sequence.recode_to_dna(),
}
}
#[inline]
#[must_use]
pub fn filter_to_dna(self) -> FastaNT {
FastaNT {
name: self.name,
sequence: self.sequence.filter_to_dna(),
}
}
#[inline]
#[must_use]
pub fn filter_to_dna_unaligned(self) -> FastaNT {
FastaNT {
name: self.name,
sequence: self.sequence.filter_to_dna_unaligned(),
}
}
#[inline]
#[must_use]
pub fn get_id_taxon(&self) -> Option<(&str, &str)> {
self.name.get_id_taxon()
}
#[must_use]
pub fn translate(self) -> FastaAA {
FastaAA {
name: self.name,
sequence: AminoAcids(nucleotides::translate_sequence(&self.sequence)),
}
}
}
impl FastaNT {
#[inline]
pub fn reverse_complement(&mut self) {
self.sequence.make_reverse_complement();
}
#[must_use]
pub fn translate(self) -> FastaAA {
FastaAA {
name: self.name,
sequence: self.sequence.translate(),
}
}
#[inline]
#[must_use]
pub fn get_id_taxon(&self) -> Option<(&str, &str)> {
self.name.get_id_taxon()
}
}
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct FastaNTAnnot {
pub name: String,
pub sequence: Nucleotides,
pub taxon: String,
}
impl TryFrom<FastaSeq> for FastaNTAnnot {
type Error = std::io::Error;
fn try_from(fa: FastaSeq) -> Result<Self, Self::Error> {
if let Some((id, taxon)) = fa.name.get_id_taxon() {
Ok(FastaNTAnnot {
name: id.to_string(),
sequence: fa.sequence.filter_to_dna(),
taxon: taxon.to_string(),
})
} else {
Err(std::io::Error::new(
ErrorKind::InvalidData,
format!("No taxon for: {id}", id = fa.name),
))
}
}
}
impl From<FastaSeq> for FastaNT {
fn from(record: FastaSeq) -> Self {
FastaNT {
name: record.name,
sequence: record.sequence.into(),
}
}
}
impl FastaAA {
#[inline]
#[must_use]
pub fn get_id_taxon(&self) -> Option<(&str, &str)> {
self.name.get_id_taxon()
}
}
impl<R: std::io::Read> FastaReader<R> {
pub fn new(inner: R) -> Self {
FastaReader {
reader: std::io::BufReader::new(inner),
buffer: Vec::new(),
first_record: true,
}
}
pub fn from_readable(read: R) -> std::io::Result<Self> {
FastaReader::from_bufreader(std::io::BufReader::new(read))
}
pub fn from_bufreader(mut reader: BufReader<R>) -> std::io::Result<Self> {
if reader.fill_buf()?.is_empty() {
return Err(IOError::new(ErrorKind::InvalidData, "No FASTA data was found!"));
}
Ok(FastaReader {
reader,
buffer: Vec::new(),
first_record: true,
})
}
fn get_error(msg: &str, header: Option<&str>) -> std::io::Result<FastaSeq> {
if let Some(header) = header {
Err(IOError::new(ErrorKind::InvalidData, format!("{msg} See header: {header}")))
} else {
Err(IOError::new(ErrorKind::InvalidData, msg))
}
}
fn read_first_record(&mut self) -> Option<std::io::Result<FastaSeq>> {
self.first_record = false;
loop {
let bytes = unwrap_or_return_some_err!(self.reader.read_until(b'\n', &mut self.buffer));
if bytes == 0 {
return Some(Self::get_error("No FASTA data found!", None));
}
if let Some(mut header) = self.buffer.strip_prefix(b">") {
header = header.strip_line_break();
if header.is_empty() {
return Some(Self::get_error("Missing FASTA header!", None));
}
let name = String::from_utf8_lossy(header).into_owned();
if header.contains(&b'>') {
return Some(Self::get_error(
"FASTA records must start with the '>' symbol on a newline, and no other '>' symbols can occur in a header!",
Some(&name),
));
}
self.buffer.clear();
unwrap_or_return_some_err!(self.reader.read_until(b'>', &mut self.buffer));
let split = self.buffer.lines_ascii::<{ DEFAULT_SIMD_LANES }>();
let mut sequence = Vec::with_capacity(split.remaining_len());
for s in split {
sequence.extend_from_slice(s);
}
if sequence.ends_with(b">") {
sequence.pop();
}
if sequence.is_empty() {
return Some(Self::get_error("Missing FASTA sequence!", Some(&name)));
}
if !self.buffer.ends_with(b"\n>") {
if self.buffer.ends_with(b">") {
return Some(Self::get_error(
"FASTA records must start with the '>' symbol on a newline, and no other '>' symbols can occur in a sequence!",
Some(&name),
));
}
self.buffer.clear();
}
return Some(Ok(FastaSeq { name, sequence }));
} else if self.buffer.iter().all(u8::is_ascii_whitespace) {
self.buffer.clear();
} else {
return Some(Self::get_error("The FASTA file must start with a '>' symbol!", None));
}
}
}
}
impl FastaReader<std::fs::File> {
pub fn from_path<P>(path: P) -> std::io::Result<FastaReader<File>>
where
P: AsRef<Path>, {
let path = path.as_ref();
let file = File::open(path).with_path_context("Failed to open path", path)?;
Ok(Self::from_readable(file).with_path_context("Failed to read data at path", path)?)
}
}
impl<R: std::io::Read> Iterator for FastaReader<R> {
type Item = std::io::Result<FastaSeq>;
fn next(&mut self) -> Option<Self::Item> {
if self.first_record {
return self.read_first_record();
}
if self.buffer.is_empty() {
return None;
}
self.buffer.clear();
unwrap_or_return_some_err!(self.reader.read_until(b'>', &mut self.buffer));
let mut split = self.buffer.lines_ascii::<{ DEFAULT_SIMD_LANES }>();
let Some(name_line) = split.next() else {
return Some(Self::get_error("Missing FASTA header!", None));
};
let name = String::from_utf8_lossy(name_line).into_owned();
if name.is_empty() {
return Some(Self::get_error("Missing FASTA header!", None));
}
let mut sequence = Vec::with_capacity(split.remaining_len());
for s in split {
sequence.extend_from_slice(s);
}
if sequence.ends_with(b">") {
sequence.pop();
}
if sequence.is_empty() {
if let Some(b'>') = self.buffer.last() {
if self.buffer.contains(&b'\n') {
return Some(Self::get_error("Missing FASTA sequence!", Some(&name)));
}
unwrap_or_return_some_err!(self.reader.read_until(b'\n', &mut self.buffer));
self.buffer.chop_line_break();
let name = String::from_utf8_lossy(&self.buffer).into_owned();
return Some(Self::get_error(
"FASTA records must start with the '>' symbol on a newline, and no other '>' symbols can occur in a header!",
Some(&name),
));
}
return Some(Self::get_error("Missing FASTA sequence!", Some(&name)));
}
if !self.buffer.ends_with(b"\n>") {
if self.buffer.ends_with(b">") {
return Some(Self::get_error(
"FASTA records must start with the '>' symbol on a newline, and no other '>' symbols can occur in a sequence!",
Some(&name),
));
}
self.buffer.clear();
}
Some(Ok(FastaSeq { name, sequence }))
}
}
impl std::fmt::Display for FastaSeq {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.sequence.is_ascii_simd::<16>() {
write!(f, ">{}\n{}\n", self.name, unsafe {
std::str::from_utf8_unchecked(&self.sequence)
})
} else {
write!(f, ">{}\n{}\n", self.name, String::from_utf8_lossy(&self.sequence))
}
}
}
impl std::fmt::Display for FastaNT {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, ">{}\n{}\n", self.name, self.sequence)
}
}
impl std::fmt::Display for FastaAA {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, ">{}\n{}\n", self.name, self.sequence)
}
}