serbzip_core/
transcoder.rs

1//! Houses reusable machinery for transcoding streams, line by line.
2
3use std::error::Error;
4use std::fmt::{Debug, Display, Formatter};
5use std::io::{BufRead, Write};
6use std::{error, io};
7
8/// Errors resulting from a transcoding operation.
9#[derive(Debug)]
10pub enum TranscodeError<L> {
11    /// When an error occurred while processing a particular line.
12    Conversion { line_no: u32, error: L },
13
14    // When the error relates to I/O.
15    Io(io::Error),
16}
17
18impl<L> TranscodeError<L> {
19    /// Maps the error into a [`TranscodeError::Conversion`].
20    pub fn into_conversion_error(self) -> Option<(u32, L)> {
21        match self {
22            TranscodeError::Conversion { line_no, error } => Some((line_no, error)),
23            TranscodeError::Io(_) => None,
24        }
25    }
26
27    /// Maps the error into a [`TranscodeError::Io`].
28    pub fn into_io_error(self) -> Option<io::Error> {
29        match self {
30            TranscodeError::Conversion { .. } => None,
31            TranscodeError::Io(error) => Some(error),
32        }
33    }
34}
35
36impl<L: Into<Box<dyn Error>>> TranscodeError<L> {
37    /// Boxes the given error if it is a [`TranscodeError::Conversion`], returning the line error as an [`Error`] trait.
38    ///
39    /// This is useful for working with methods that return a generic [`Result<_, Box<dyn Error>>`].
40    pub fn into_dynamic(self) -> TranscodeError<Box<dyn Error>> {
41        match self {
42            TranscodeError::Conversion { line_no, error } => TranscodeError::Conversion {
43                line_no,
44                error: error.into(),
45            },
46            TranscodeError::Io(error) => TranscodeError::Io(error),
47        }
48    }
49}
50
51impl<L: Display + Debug> Display for TranscodeError<L> {
52    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53        match self {
54            TranscodeError::Conversion { line_no, error } => {
55                write!(f, "conversion error on line {line_no}: {error}")
56            }
57            TranscodeError::Io(error) => write!(f, "I/O error: {error:?}"),
58        }
59    }
60}
61
62impl<L: Display + Debug> error::Error for TranscodeError<L> {}
63
64impl<L> From<io::Error> for TranscodeError<L> {
65    fn from(error: io::Error) -> Self {
66        TranscodeError::Io(error)
67    }
68}
69
70/// Transcodes between a buffered reader and a writer, using a given `processor` closure to
71/// perform the line-by-line translation.
72///
73/// This function does not require that the output writer be buffered, although a buffered
74/// writer may be supplied for performance.
75///
76/// # Errors
77/// [`TranscodeError<L>`] is returned if either the reader/writer produced an I/O error, or
78/// the processor returned an error. In the latter, the error encompasses the line number.
79pub fn transcode<L>(
80    r: &mut impl BufRead,
81    w: &mut impl Write,
82    mut processor: impl FnMut(u32, &str) -> Result<String, L>,
83) -> Result<(), TranscodeError<L>> {
84    let mut read_buf = String::new();
85    let mut line_no = 1u32;
86    loop {
87        match r.read_line(&mut read_buf)? {
88            0 => return Ok(()),
89            size => match processor(line_no, &read_buf[0..size - 1]) {
90                Ok(output) => {
91                    writeln!(w, "{}", output)?;
92                    read_buf.clear();
93                }
94                Err(error) => return Err(TranscodeError::Conversion { line_no, error }),
95            },
96        }
97        line_no += 1;
98    }
99}
100
101#[cfg(test)]
102mod tests;