Skip to content

Commit 05aeefb

Browse files
committed
Add more context to paired read errors in xleave
1 parent 0eb299b commit 05aeefb

3 files changed

Lines changed: 140 additions & 65 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ is roughly based on [Keep a Changelog], and this project tries to adheres to
1313

1414
### Changed
1515

16-
- Adds more context to paired read errors in `sampler`
16+
- Adds more context to paired read errors in `sampler` and `xleave`
1717

1818
## [0.9.0] - 2026-03-06
1919

src/processes/standalone/xleave.rs

Lines changed: 38 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
//! Interleaves or de-interleaves paired FastQ or FASTA files.
22
33
use crate::{
4-
io::{
5-
DispatchFastX, InputOptions, OutputOptions, RecordWriters, WriteFileZipStdout, WriteRecord, WriteRecords,
6-
check_distinct_files,
7-
},
4+
io::{DispatchFastX, InputOptions, OutputOptions, RecordWriters, WriteRecords, check_distinct_files},
85
utils::paired_reads::{DeinterleavedPairedReadsExt, ZipPairedReadsExt},
96
};
107
use clap::Args;
118
use std::path::PathBuf;
12-
use zoe::data::records::HeaderReadable;
139

1410
#[derive(Args, Debug)]
1511
pub struct XleaveArgs {
@@ -46,61 +42,54 @@ pub fn xleave_process(args: XleaveArgs) -> Result<(), std::io::Error> {
4642
.use_file_zip_or_stdout()
4743
.open()?;
4844

49-
match (readers.reader1.dispatch(), readers.reader2.map(|x| x.dispatch())) {
50-
(DispatchFastX::Fastq(reader), None) => match writer {
51-
RecordWriters::SingleEnd(_) => {
52-
return Err(std::io::Error::other(
53-
"One input and one output were provided. No interleaving or de-interleaving can occur.",
54-
));
55-
}
56-
RecordWriters::PairedEnd(_) => handle_single_input(reader, writer)?,
57-
},
58-
(DispatchFastX::Fastq(reader1), Some(DispatchFastX::Fastq(reader2))) => match writer {
59-
RecordWriters::SingleEnd(_) => reader1.zip_paired_reads(reader2).write_records(writer)?,
60-
RecordWriters::PairedEnd(_) => {
61-
return Err(std::io::Error::other(
62-
"Two inputs and two outputs were provided. No interleaving or de-interleaving can occur.",
63-
));
64-
}
65-
},
66-
(DispatchFastX::Fasta(reader), None) => match writer {
67-
RecordWriters::SingleEnd(_) => {
45+
let reader1 = readers.reader1;
46+
let input_path1 = args.input_file1;
47+
48+
if let Some((reader2, input_path2)) = readers.reader2.zip(args.input_file2) {
49+
let RecordWriters::SingleEnd(writer) = writer else {
50+
return Err(std::io::Error::other(
51+
"Two inputs and two outputs were provided. No interleaving or de-interleaving can occur.",
52+
));
53+
};
54+
55+
match (reader1.dispatch(), reader2.dispatch()) {
56+
(DispatchFastX::Fastq(reader1), DispatchFastX::Fastq(reader2)) => reader1
57+
.zip_paired_reads(reader2)
58+
.map(|res| res.map_err(|e| e.add_path_context(&input_path1, &input_path2)))
59+
.write_records(writer)?,
60+
(DispatchFastX::Fasta(reader1), DispatchFastX::Fasta(reader2)) => reader1
61+
.zip_paired_reads(reader2)
62+
.map(|res| res.map_err(|e| e.add_path_context(&input_path1, &input_path2)))
63+
.write_records(writer)?,
64+
(DispatchFastX::Fastq(_), DispatchFastX::Fasta(_)) => {
6865
return Err(std::io::Error::other(
69-
"One input and one output were provided. No interleaving or de-interleaving can occur.",
66+
"Paired read inputs must be both FASTQ or both FASTA. Found FASTQ for first input and FASTA for second input.",
7067
));
7168
}
72-
RecordWriters::PairedEnd(_) => handle_single_input(reader, writer)?,
73-
},
74-
(DispatchFastX::Fasta(reader1), Some(DispatchFastX::Fasta(reader2))) => match writer {
75-
RecordWriters::SingleEnd(_) => reader1.zip_paired_reads(reader2).write_records(writer)?,
76-
RecordWriters::PairedEnd(_) => {
69+
(DispatchFastX::Fasta(_), DispatchFastX::Fastq(_)) => {
7770
return Err(std::io::Error::other(
78-
"Two inputs and two outputs were provided. No interleaving or de-interleaving can occur.",
71+
"Paired read inputs must be both FASTQ or both FASTA. Found FASTA for first input and FASTQ for second input.",
7972
));
8073
}
81-
},
82-
(DispatchFastX::Fastq(_), Some(DispatchFastX::Fasta(_))) => {
83-
return Err(std::io::Error::other(
84-
"Paired read inputs must be both FASTQ or both FASTA. Found FASTQ for first input and FASTA for second input.",
85-
));
8674
}
87-
(DispatchFastX::Fasta(_), Some(DispatchFastX::Fastq(_))) => {
75+
} else {
76+
let RecordWriters::PairedEnd(writer) = writer else {
8877
return Err(std::io::Error::other(
89-
"Paired read inputs must be both FASTQ or both FASTA. Found FASTA for first input and FASTQ for second input.",
78+
"One input and one output were provided. No interleaving or de-interleaving can occur.",
9079
));
80+
};
81+
82+
match reader1.dispatch() {
83+
DispatchFastX::Fastq(reader) => reader
84+
.deinterleave()
85+
.map(|res| res.map_err(|e| e.add_path_context(&input_path1)))
86+
.write_records(writer)?,
87+
DispatchFastX::Fasta(reader) => reader
88+
.deinterleave()
89+
.map(|res| res.map_err(|e| e.add_path_context(&input_path1)))
90+
.write_records(writer)?,
9191
}
9292
}
9393

9494
Ok(())
9595
}
96-
97-
fn handle_single_input<R1, A>(reader: R1, writer: RecordWriters<WriteFileZipStdout>) -> std::io::Result<()>
98-
where
99-
R1: Iterator<Item = std::io::Result<A>>,
100-
A: HeaderReadable + WriteRecord<WriteFileZipStdout>,
101-
std::io::Result<A>: WriteRecord<WriteFileZipStdout>, {
102-
match writer {
103-
RecordWriters::SingleEnd(writer) => reader.write_records(writer),
104-
RecordWriters::PairedEnd(writer) => reader.deinterleave().write_records(writer),
105-
}
106-
}

src/utils/paired_reads.rs

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ where
177177
A: HeaderReadable,
178178
{
179179
/// Maps the error to include messages and context with the provided paths.
180+
///
181+
/// It is assumed that IO errors already have path context included.
180182
pub fn add_path_context(self, path1: &Path, path2: &Path) -> std::io::Error {
181183
match self {
182184
ZipReadsError::IoError(e) => e,
@@ -199,6 +201,8 @@ where
199201
A: HeaderReadable,
200202
{
201203
/// Maps the error to include messages and context with the provided paths.
204+
///
205+
/// It is assumed that IO errors already have path context included.
202206
pub fn add_path_context(self, path1: &Path, path2: &Path) -> std::io::Error {
203207
match self {
204208
ZipPairedReadsError::IoError(e) => e,
@@ -539,6 +543,96 @@ where
539543
{
540544
}
541545

546+
/// The error type for [`DeinterleavedPairedReads`].
547+
#[derive(Debug)]
548+
pub enum DeinterleaveError<A> {
549+
/// An IO error from the reader
550+
IoError(std::io::Error),
551+
/// A mismatch in the header IDs of the paired subsequent reads
552+
MismatchedHeaders([A; 2]),
553+
/// An odd number of reads in the iterator
554+
OddNumberOfReads(A),
555+
}
556+
557+
impl<A> From<std::io::Error> for DeinterleaveError<A> {
558+
#[inline]
559+
fn from(value: std::io::Error) -> Self {
560+
Self::IoError(value)
561+
}
562+
}
563+
564+
impl<A: HeaderReadable> From<DeinterleaveError<A>> for std::io::Error {
565+
#[inline]
566+
fn from(value: DeinterleaveError<A>) -> Self {
567+
match value {
568+
DeinterleaveError::IoError(e) => e,
569+
other => std::io::Error::other(other.to_string()),
570+
}
571+
}
572+
}
573+
574+
impl<A: HeaderReadable> Display for DeinterleaveError<A> {
575+
#[inline]
576+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
577+
match self {
578+
DeinterleaveError::IoError(e) => write!(f, "{e}"),
579+
DeinterleaveError::MismatchedHeaders([r1, r2]) => write!(
580+
f,
581+
"Paired read IDs out of sync:\n\t{h1}\n\t{h2}\n",
582+
h1 = r1.header(),
583+
h2 = r2.header()
584+
),
585+
DeinterleaveError::OddNumberOfReads(r1) => write!(
586+
f,
587+
"An odd number of reads was found while de-interleaving. See header: {header1}",
588+
header1 = r1.header()
589+
),
590+
}
591+
}
592+
}
593+
594+
impl<A: HeaderReadable + Debug> Error for DeinterleaveError<A> {
595+
fn source(&self) -> Option<&(dyn Error + 'static)> {
596+
match self {
597+
DeinterleaveError::IoError(e) => e.source(),
598+
_ => None,
599+
}
600+
}
601+
}
602+
603+
impl<A: HeaderReadable> GetCode for DeinterleaveError<A> {
604+
fn get_code(&self) -> i32 {
605+
match self {
606+
DeinterleaveError::IoError(e) => e.get_code(),
607+
_ => 1,
608+
}
609+
}
610+
}
611+
612+
impl<A> DeinterleaveError<A>
613+
where
614+
A: HeaderReadable,
615+
{
616+
/// Maps the error to include messages and context with the provided path.
617+
///
618+
/// It is assumed that IO errors already have path context included.
619+
pub fn add_path_context(self, path: &Path) -> std::io::Error {
620+
match self {
621+
DeinterleaveError::IoError(e) => e,
622+
DeinterleaveError::MismatchedHeaders([r1, r2]) => std::io::Error::other(format!(
623+
"Paired read IDs out of sync:\n| Header 1: {header1}\n| Header 2: {header2}",
624+
header1 = r1.header(),
625+
header2 = r2.header(),
626+
))
627+
.with_file_context("Failed to deinterleave the reads in file", path)
628+
.into(),
629+
e @ DeinterleaveError::OddNumberOfReads(_) => std::io::Error::from(e)
630+
.with_file_context("Failed to deinterleave the reads in file", path)
631+
.into(),
632+
}
633+
}
634+
}
635+
542636
pub struct DeinterleavedPairedReads<I, A>(I)
543637
where
544638
I: Iterator<Item = std::io::Result<A>>,
@@ -549,29 +643,21 @@ where
549643
I: Iterator<Item = std::io::Result<A>>,
550644
A: HeaderReadable,
551645
{
552-
type Item = std::io::Result<[A; 2]>;
646+
type Item = Result<[A; 2], DeinterleaveError<A>>;
553647

554648
fn next(&mut self) -> Option<Self::Item> {
555-
let read1 = unwrap_or_return_some_err!(self.0.next()?);
649+
let read1 = unwrap_or_return_some_err!(self.0.next()?.map_err(DeinterleaveError::IoError));
650+
556651
if let Some(read2) = self.0.next() {
557-
let read2 = unwrap_or_return_some_err!(read2);
652+
let read2 = unwrap_or_return_some_err!(read2.map_err(DeinterleaveError::IoError));
653+
558654
if check_paired_headers(&read1, &read2).is_ok() {
559655
Some(Ok([read1, read2]))
560656
} else {
561-
Some(Err(IOError::new(
562-
ErrorKind::InvalidInput,
563-
format!(
564-
"Paired read IDs out of sync:\n\t{h1}\n\t{h2}\n",
565-
h1 = read1.header(),
566-
h2 = read2.header()
567-
),
568-
)))
657+
Some(Err(DeinterleaveError::MismatchedHeaders([read1, read2])))
569658
}
570659
} else {
571-
Some(Err(std::io::Error::other(format!(
572-
"An odd number of reads was found while de-interleaving: {header1}",
573-
header1 = read1.header()
574-
))))
660+
Some(Err(DeinterleaveError::OddNumberOfReads(read1)))
575661
}
576662
}
577663
}

0 commit comments

Comments
 (0)