Skip to content

Commit c4acc61

Browse files
committed
Add WriterWithContext
1 parent deda8a4 commit c4acc61

5 files changed

Lines changed: 155 additions & 52 deletions

File tree

src/io/mod.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use std::{fmt::Display, io::Read, path::Path};
1+
use std::{
2+
fmt::Display,
3+
io::{Read, Write},
4+
path::Path,
5+
};
26
use zoe::{
37
data::err::{ResultWithErrorContext, WithErrorContext},
48
prelude::{FastQReader, FastaReader},
@@ -98,7 +102,7 @@ pub trait IterWithErrorContext: Sized {
98102
/// [`ErrorWithContext`]: zoe::data::err::ErrorWithContext
99103
fn iter_with_context(self, description: impl Into<String>) -> IterWithContext<Self>;
100104

101-
/// Convenience function for adding file context to an yielded errors.
105+
/// Convenience function for adding file context to any yielded errors.
102106
///
103107
/// The context will be formatted as `msg: file`. The `msg` field may be
104108
/// anything implementing [`Display`].
@@ -232,3 +236,73 @@ pub(crate) fn check_distinct_files(
232236
pub(crate) fn is_gz<P: AsRef<Path>>(path: P) -> bool {
233237
path.as_ref().extension().is_some_and(|ext| ext == "gz")
234238
}
239+
240+
/// A wrapper around a writer of type `W` such that error context is added to
241+
/// any failed writes.
242+
#[derive(Debug)]
243+
pub struct WriterWithContext<W> {
244+
/// The inner writer.
245+
writer: W,
246+
/// The context to add to any failed writes.
247+
description: String,
248+
}
249+
250+
impl<W> Write for WriterWithContext<W>
251+
where
252+
W: Write,
253+
{
254+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
255+
Ok(self.writer.write(buf).with_context(&self.description)?)
256+
}
257+
258+
fn flush(&mut self) -> std::io::Result<()> {
259+
Ok(self.writer.flush().with_context(&self.description)?)
260+
}
261+
262+
fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
263+
Ok(self.writer.write_vectored(bufs).with_context(&self.description)?)
264+
}
265+
266+
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
267+
Ok(self.writer.write_all(buf).with_context(&self.description)?)
268+
}
269+
270+
fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> {
271+
Ok(self.writer.write_fmt(args).with_context(&self.description)?)
272+
}
273+
}
274+
275+
/// An extension trait for [`Write`] allowing additional context to be added to
276+
/// each failed write (via [`WriterWithContext`]).
277+
pub trait WriterWithErrorContext: Sized {
278+
/// Wraps any errors that get produced during writing in an
279+
/// [`ErrorWithContext`] with the given description.
280+
///
281+
/// The `description` field may be anything implementing `Into<String>`.
282+
/// Passing an owned `String` avoids an extra allocation.
283+
///
284+
/// [`ErrorWithContext`]: zoe::data::err::ErrorWithContext
285+
fn writer_with_context(self, description: impl Into<String>) -> WriterWithContext<Self>;
286+
287+
/// Convenience function for adding file context to any produced errors.
288+
///
289+
/// The context will be formatted as `msg: file`. The `msg` field may be
290+
/// anything implementing [`Display`].
291+
fn writer_with_file_context(self, msg: impl Display, file: impl AsRef<Path>) -> WriterWithContext<Self>;
292+
}
293+
294+
impl<W> WriterWithErrorContext for W
295+
where
296+
W: Write,
297+
{
298+
fn writer_with_context(self, description: impl Into<String>) -> WriterWithContext<Self> {
299+
WriterWithContext {
300+
writer: self,
301+
description: description.into(),
302+
}
303+
}
304+
305+
fn writer_with_file_context(self, msg: impl Display, file: impl AsRef<Path>) -> WriterWithContext<Self> {
306+
Self::writer_with_context(self, format!("{msg}: '{path}'", path = file.as_ref().display()))
307+
}
308+
}

src/io/open_options/context.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::io::{IterWithContext, IterWithErrorContext};
2-
use std::{error::Error, fmt::Display, path::Path};
1+
use crate::io::{IterWithContext, IterWithErrorContext, WriterWithContext, WriterWithErrorContext};
2+
use std::{error::Error, fmt::Display, io::Write, path::Path};
33
use zoe::data::err::{ErrorWithContext, WithErrorContext};
44

55
/// An enum to represent the possible reader types for [`InputOptions`], for the
@@ -203,11 +203,11 @@ impl InputContext<'_> {
203203
/// [`OutputOptions`]: crate::io::open_options::OutputOptions
204204
pub struct OutputContext<'a> {
205205
/// The [`OutputType`] used by the first output.
206-
output1: OutputType<'a>,
206+
pub output1: OutputType<'a>,
207207
/// The [`OutputType`] used by the second output. If there is no second
208208
/// output, this defaults to [`OutputType::Stdout`], but it will not
209209
/// influence the error context displayed.
210-
output2: OutputType<'a>,
210+
pub output2: OutputType<'a>,
211211
}
212212

213213
impl<'a> OutputContext<'a> {
@@ -241,6 +241,17 @@ impl OutputContext<'_> {
241241
OutputType::Stdout => e.with_context("Failed to write to stdout"),
242242
}
243243
}
244+
245+
/// Given one of the `output` fields for an [`OutputContext`], add the
246+
/// context to a provided writer.
247+
pub fn add_writer_context<W>(writer: W, output: OutputType) -> WriterWithContext<W>
248+
where
249+
W: Write, {
250+
match output {
251+
OutputType::File(path) => writer.writer_with_file_context("Failed to write to file", path),
252+
OutputType::Stdout => writer.writer_with_context("Failed to write to stdout"),
253+
}
254+
}
244255
}
245256

246257
/// An error occurring while working with potentially-paired inputs or outputs,

src/io/open_options/output_options.rs

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::io::{OptionalPaths, OutputContext, PairedErrors, RecordWriters, WriteFileZipStdout, open_options::PairedStruct};
1+
use crate::io::{
2+
OptionalPaths, OutputContext, PairedErrors, RecordWriters, WriteFileZipStdout, WriterWithContext,
3+
open_options::PairedStruct,
4+
};
25
use std::{fs::File, io::BufWriter, path::Path};
36

47
/// A builder pattern for creating output files in IRMA-core.
@@ -89,16 +92,8 @@ impl<'a> OutputOptions<'a, &'a Path> {
8992
}
9093

9194
/// Interprets the path using [`File`] for writing.
92-
#[allow(dead_code)]
93-
pub fn use_file(self) -> OutputOptions<'a, BufWriter<File>> {
94-
let output = self.output.and_then(|path| {
95-
if let Some(capacity) = self.capacity {
96-
File::create(path).map(|file| BufWriter::with_capacity(capacity, file))
97-
} else {
98-
File::create(path).map(BufWriter::new)
99-
}
100-
.map_err(PairedErrors::Err1)
101-
});
95+
pub fn use_file(self) -> OutputOptions<'a, File> {
96+
let output = self.output.and_then(|path| File::create(path).map_err(PairedErrors::Err1));
10297

10398
OutputOptions {
10499
context: self.context,
@@ -184,18 +179,10 @@ impl<'a> OutputOptions<'a, RecordWriters<&'a Path>> {
184179

185180
/// Interprets the path(s) using [`File`] for writing.
186181
#[allow(dead_code)]
187-
pub fn use_file(self) -> OutputOptions<'a, RecordWriters<BufWriter<File>>> {
182+
pub fn use_file(self) -> OutputOptions<'a, RecordWriters<File>> {
188183
OutputOptions {
189184
context: self.context,
190-
output: self.output.and_then(|writers| {
191-
writers.try_map(|path| {
192-
if let Some(capacity) = self.capacity {
193-
File::create(path).map(|file| BufWriter::with_capacity(capacity, file))
194-
} else {
195-
File::create(path).map(BufWriter::new)
196-
}
197-
})
198-
}),
185+
output: self.output.and_then(|writers| writers.try_map(File::create)),
199186
capacity: self.capacity,
200187
}
201188
}
@@ -252,16 +239,26 @@ impl<'a> OutputOptions<'a, OptionalPaths<'a>> {
252239
}
253240
}
254241

255-
impl<'a> OutputOptions<'a, BufWriter<File>> {
242+
impl<'a> OutputOptions<'a, File> {
256243
/// Opens the [`File`] for writing, wrapping it in a [`BufWriter`].
257244
///
258245
/// ## Errors
259246
///
260247
/// IO errors when opening the file are propagated. Context is added that
261248
/// includes the path.
262-
pub fn open(self) -> std::io::Result<BufWriter<File>> {
263-
match self.output {
264-
Ok(writer) => Ok(writer),
249+
pub fn open(self) -> std::io::Result<BufWriter<WriterWithContext<File>>> {
250+
let output = self.output.map(|file| {
251+
let output = OutputContext::add_writer_context(file, self.context.output1);
252+
253+
if let Some(capacity) = self.capacity {
254+
BufWriter::with_capacity(capacity, output)
255+
} else {
256+
BufWriter::new(output)
257+
}
258+
});
259+
260+
match output {
261+
Ok(output) => Ok(output),
265262
Err(e) => Err(self.context.add_context(e).into()),
266263
}
267264
}
@@ -282,7 +279,7 @@ impl<'a> OutputOptions<'a, WriteFileZipStdout> {
282279
}
283280
}
284281

285-
impl<'a> OutputOptions<'a, RecordWriters<BufWriter<File>>> {
282+
impl<'a> OutputOptions<'a, RecordWriters<File>> {
286283
/// Opens the potentially paired [`File`]s for writing, wrapping each in a
287284
/// [`BufWriter`].
288285
///
@@ -292,8 +289,18 @@ impl<'a> OutputOptions<'a, RecordWriters<BufWriter<File>>> {
292289
/// includes the path.
293290
#[allow(dead_code)]
294291
pub fn open(self) -> std::io::Result<RecordWriters<BufWriter<File>>> {
295-
match self.output {
296-
Ok(writer) => Ok(writer),
292+
let output = self.output.map(|writers| {
293+
writers.map(|file| {
294+
if let Some(capacity) = self.capacity {
295+
BufWriter::with_capacity(capacity, file)
296+
} else {
297+
BufWriter::new(file)
298+
}
299+
})
300+
});
301+
302+
match output {
303+
Ok(output) => Ok(output),
297304
Err(e) => Err(self.context.add_context(e).into()),
298305
}
299306
}

src/io/writers.rs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::io::is_gz;
1+
use crate::io::{WriterWithContext, WriterWithErrorContext, is_gz};
22
use flate2::{Compression, write::GzEncoder};
33
use std::{
44
fs::File,
@@ -9,15 +9,16 @@ use zoe::define_whichever;
99

1010
define_whichever! {
1111
/// An enum for the different acceptable output types. A [`BufWriter`] is
12-
/// used for all variants.
12+
/// used for all variants, and all variants are wrapped in
13+
/// [`WriterWithContext`] to context to write errors.
1314
#[derive(Debug)]
1415
pub(crate) enum WriteFileZipStdout {
1516
/// A writer for a regular uncompressed file.
16-
File(BufWriter<File>),
17+
File(WriterWithContext<BufWriter<File>>),
1718
/// A writer for a gzip compressed file.
18-
Zipped(GzEncoder<BufWriter<File>>),
19+
Zipped(WriterWithContext<GzEncoder<BufWriter<File>>>),
1920
/// A writer for uncompressed data to stdout.
20-
Stdout(BufWriter<Stdout>),
21+
Stdout(WriterWithContext<BufWriter<Stdout>>),
2122
}
2223

2324
impl Write for WriteFileZipStdout {}
@@ -37,15 +38,20 @@ impl WriteFileZipStdout {
3738
let file = File::create(&path)?;
3839
let bufwriter = BufWriter::new(file);
3940

40-
let writer = if is_gz(path) {
41-
Self::Zipped(GzEncoder::new(bufwriter, Compression::default()))
41+
let writer = if is_gz(&path) {
42+
Self::Zipped(
43+
GzEncoder::new(bufwriter, Compression::default())
44+
.writer_with_file_context("Failed to write to zipped file", path),
45+
)
4246
} else {
43-
Self::File(bufwriter)
47+
Self::File(bufwriter.writer_with_file_context("Failed to write to file", path))
4448
};
4549

4650
Ok(writer)
4751
}
48-
None => Ok(WriteFileZipStdout::Stdout(BufWriter::new(stdout()))),
52+
None => Ok(WriteFileZipStdout::Stdout(
53+
BufWriter::new(stdout()).writer_with_context("Failed to write to stdout"),
54+
)),
4955
}
5056
}
5157

@@ -57,15 +63,20 @@ impl WriteFileZipStdout {
5763
let file = File::create(&path)?;
5864
let bufwriter = BufWriter::with_capacity(capacity, file);
5965

60-
let writer = if is_gz(path) {
61-
Self::Zipped(GzEncoder::new(bufwriter, Compression::default()))
66+
let writer = if is_gz(&path) {
67+
Self::Zipped(
68+
GzEncoder::new(bufwriter, Compression::default())
69+
.writer_with_file_context("Failed to write to zipped file", path),
70+
)
6271
} else {
63-
Self::File(bufwriter)
72+
Self::File(bufwriter.writer_with_file_context("Failed to write to file", path))
6473
};
6574

6675
Ok(writer)
6776
}
68-
None => Ok(WriteFileZipStdout::Stdout(BufWriter::with_capacity(capacity, stdout()))),
77+
None => Ok(WriteFileZipStdout::Stdout(
78+
BufWriter::with_capacity(capacity, stdout()).writer_with_context("Failed to write to stdout"),
79+
)),
6980
}
7081
}
7182
}

src/processes/integrated/preprocess.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use crate::{
66
args::clipping::{ClippingArgs, ParsedClippingArgs, parse_clipping_args},
7-
io::{InputOptions, IterWithContext, OutputOptions, ReadFileZipPipe, RecordReaders},
7+
io::{InputOptions, IterWithContext, OutputOptions, ReadFileZipPipe, RecordReaders, WriterWithContext},
88
qc::{fastq::ReadTransforms, fastq_metadata::*},
99
utils::{
1010
get_hasher,
@@ -103,10 +103,10 @@ pub fn preprocess_process(args: PreprocessArgs) -> Result<(), std::io::Error> {
103103
}
104104

105105
struct ParsedPreprocessIoArgs {
106-
table_writer: BufWriter<File>,
106+
table_writer: BufWriter<WriterWithContext<File>>,
107107
reader1: IterWithContext<FastQReader<ReadFileZipPipe>>,
108108
reader2: Option<IterWithContext<FastQReader<ReadFileZipPipe>>>,
109-
log_writer: Option<BufWriter<File>>,
109+
log_writer: Option<BufWriter<WriterWithContext<File>>>,
110110
log_file: Option<PathBuf>,
111111
}
112112

@@ -253,7 +253,7 @@ fn trim_and_deflate(
253253
/// Writes the table file to `table_writer` and the XFL file to STDOUT. The
254254
/// number of read patterns is returned.
255255
fn output_deflated_sequences(
256-
metadata_by_sequence: DeflatedSequences, mut table_writer: BufWriter<File>,
256+
metadata_by_sequence: DeflatedSequences, mut table_writer: impl Write,
257257
) -> Result<usize, std::io::Error> {
258258
let mut stdout_writer = BufWriter::new(std::io::stdout());
259259

@@ -282,7 +282,7 @@ fn output_deflated_sequences(
282282

283283
/// Writes the log file.
284284
fn write_log(
285-
mut log_writer: BufWriter<File>, metadata: &FastQMetadata, paired_reads: bool, read_pattern_count_passing: usize,
285+
mut log_writer: impl Write, metadata: &FastQMetadata, paired_reads: bool, read_pattern_count_passing: usize,
286286
options: &ParsedPreprocessOptions, log_file: PathBuf,
287287
) -> Result<(), std::io::Error> {
288288
let FastQMetadata {

0 commit comments

Comments
 (0)