use std::{
borrow::Cow,
error::Error,
fmt,
io::{self, BufWriter, Write},
};
use clap::ValueEnum;
use crate::{
cli::LaunchTarget,
report,
unicode::{CodePoint, representation::TextFormat},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
Plain,
Json,
JsonString,
RustString,
}
#[derive(Debug)]
pub enum OutputError {
NonScalar(CodePoint),
Write(io::Error),
}
impl fmt::Display for OutputError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonScalar(point) => write!(
formatter,
"cannot output {point} as a string: not a Unicode scalar value"
),
Self::Write(error) => write!(formatter, "failed to write stdout: {error}"),
}
}
}
impl Error for OutputError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::NonScalar(_) => None,
Self::Write(error) => Some(error),
}
}
}
pub fn write(
format: OutputFormat,
target: &LaunchTarget,
writer: impl Write,
) -> Result<(), OutputError> {
let mut writer = BufWriter::new(writer);
let result = match format {
OutputFormat::Plain => report::plain::write(target, &mut writer),
OutputFormat::Json => report::write(target, &mut writer),
OutputFormat::JsonString | OutputFormat::RustString => {
let text = match target {
LaunchTarget::CodePoint(point) => Cow::Owned(
point
.to_char()
.ok_or(OutputError::NonScalar(*point))?
.to_string(),
),
LaunchTarget::Sequence(text) => Cow::Borrowed(text.as_str()),
};
let format = match format {
OutputFormat::JsonString => TextFormat::JsonString,
OutputFormat::RustString => TextFormat::RustString,
OutputFormat::Plain | OutputFormat::Json => unreachable!(),
};
write_literal(format, &text, &mut writer)
}
}
.and_then(|()| {
writer.write_all(b"\n")?;
writer.flush()
});
match result {
Err(error) if error.kind() == io::ErrorKind::BrokenPipe => Ok(()),
result => result.map_err(OutputError::Write),
}
}
fn write_literal(format: TextFormat, text: &str, mut writer: impl Write) -> io::Result<()> {
for atom in format.atoms(text) {
writer.write_all(atom.as_bytes())?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
struct FailingWriter {
kind: io::ErrorKind,
on_flush: bool,
}
impl Write for FailingWriter {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
if self.on_flush {
Ok(bytes.len())
} else {
Err(self.kind.into())
}
}
fn flush(&mut self) -> io::Result<()> {
Err(self.kind.into())
}
}
#[rstest]
#[case::write(false)]
#[case::flush(true)]
fn handles_write_and_flush_failures(#[case] on_flush: bool) {
let target = LaunchTarget::Sequence("A".repeat(32));
for kind in [io::ErrorKind::Other, io::ErrorKind::BrokenPipe] {
let result = write(
OutputFormat::Json,
&target,
FailingWriter { kind, on_flush },
);
if kind == io::ErrorKind::BrokenPipe {
assert!(result.is_ok());
} else {
assert!(matches!(result, Err(OutputError::Write(error)) if error.kind() == kind));
}
}
}
}