Skip to content

Commit

Permalink
Add public error constructors (#7)
Browse files Browse the repository at this point in the history
Closes #4
  • Loading branch information
9999years authored Mar 21, 2024
1 parent d05721d commit e206a65
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 22 deletions.
5 changes: 4 additions & 1 deletion src/debug_display.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use std::fmt::Debug;
use std::fmt::Display;

pub(crate) trait DebugDisplay: Debug + Display {}
/// A type that implements [`Debug`] and [`Display`].
///
/// Used to store formattable and debuggable objects in [`Box<dyn DebugDisplay>`] containers.
pub trait DebugDisplay: Debug + Display {}

impl<T> DebugDisplay for T where T: Debug + Display {}
34 changes: 33 additions & 1 deletion src/exec_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,40 @@ use crate::OutputError;
///
/// This is a command that fails to start, rather than a command that exits with a non-zero status
/// or similar, like [`OutputError`].
///
/// ```
/// # use pretty_assertions::assert_eq;
/// # use std::process::Command;
/// # use command_error::Utf8ProgramAndArgs;
/// # use command_error::CommandDisplay;
/// # use command_error::ExecError;
/// let mut command = Command::new("echo");
/// command.arg("puppy doggy");
/// let displayed: Utf8ProgramAndArgs = (&command).into();
/// let error = ExecError::new(
/// Box::new(displayed),
/// std::io::Error::new(
/// std::io::ErrorKind::NotFound,
/// "File not found (os error 2)"
/// ),
/// );
/// assert_eq!(
/// error.to_string(),
/// "Failed to execute `echo`: File not found (os error 2)"
/// );
/// ```
pub struct ExecError {
pub(crate) command: Box<dyn CommandDisplay>,
pub(crate) inner: std::io::Error,
}

impl ExecError {
/// Construct a new [`ExecError`].
pub fn new(command: Box<dyn CommandDisplay>, inner: std::io::Error) -> Self {
Self { command, inner }
}
}

impl Debug for ExecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExecError")
Expand All @@ -27,9 +56,12 @@ impl Debug for ExecError {

impl Display for ExecError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let program = self.command.program();
#[cfg(feature = "shell-words")]
let program = shell_words::quote(&program);
// TODO: Should this contain an additional message like
// "Is `program` installed and present in your `$PATH`?"
write!(f, "Failed to execute `{}`: {}", self.command, self.inner)
write!(f, "Failed to execute `{program}`: {}", self.inner)
}
}

Expand Down
5 changes: 2 additions & 3 deletions src/output_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ where
E: Debug + Display + 'static,
{
Error::from(
OutputError::new(self.command, Box::new(self.output))
.with_user_error(Some(Box::new(message))),
OutputError::new(self.command, Box::new(self.output)).with_message(Box::new(message)),
)
}

Expand All @@ -79,7 +78,7 @@ where
{
let ret = OutputError::new(self.command, Box::new(self.output));
Error::from(match message {
Some(message) => ret.with_user_error(Some(Box::new(message))),
Some(message) => ret.with_message(Box::new(message)),
None => ret,
})
}
Expand Down
38 changes: 37 additions & 1 deletion src/output_conversion_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,49 @@ use crate::CommandDisplay;
use crate::CommandExt;

/// An error produced when attempting to convert [`Command`] [`Output`] to a custom format (such as
/// [`Utf8Output`]). Produced by methods like [`CommandExt::output_checked_with`] and
/// [`Utf8Output`]).
///
/// Produced by methods like [`CommandExt::output_checked_with`] and
/// [`CommandExt::output_checked_utf8`].
///
/// ```
/// # use pretty_assertions::assert_eq;
/// # use indoc::indoc;
/// # use std::process::Command;
/// # use std::process::Output;
/// # use std::process::ExitStatus;
/// # use command_error::Utf8ProgramAndArgs;
/// # use command_error::CommandDisplay;
/// # use command_error::OutputConversionError;
/// let mut command = Command::new("sh");
/// command.args(["-c", "echo puppy doggy"]);
/// let displayed: Utf8ProgramAndArgs = (&command).into();
/// let mut output = command.output().unwrap();
/// output.stdout[5] = 0xc0; // Invalid UTF-8 byte.
/// let inner: Result<utf8_command::Utf8Output, _> = output.try_into();
/// let error = OutputConversionError::new(
/// Box::new(displayed),
/// Box::new(inner.unwrap_err())
/// );
/// assert_eq!(
/// error.to_string(),
/// "Failed to convert `sh` output: \
/// Stdout contained invalid utf-8 sequence of 1 bytes from index 5: \
/// \"puppy�doggy\\n\""
/// );
/// ```
pub struct OutputConversionError {
pub(crate) command: Box<dyn CommandDisplay>,
pub(crate) inner: Box<dyn Display>,
}

impl OutputConversionError {
/// Construct a new [`OutputConversionError`].
pub fn new(command: Box<dyn CommandDisplay>, inner: Box<dyn Display>) -> Self {
Self { command, inner }
}
}

impl Debug for OutputConversionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OutputConversionError")
Expand Down
87 changes: 71 additions & 16 deletions src/output_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,54 @@ use crate::OutputLike;
#[cfg(doc)]
use crate::CommandExt;

/// An error from a failed command. Produced by [`CommandExt`].
#[cfg(doc)]
use crate::ExecError;

/// An error from a failed command, typically due to a non-zero exit status.
///
/// Produced by [`CommandExt`]. This indicates a command that failed, typically with a non-zero
/// exit code, rather than a command that failed to start (as in [`ExecError`]).
///
/// ```
/// # use pretty_assertions::assert_eq;
/// # use indoc::indoc;
/// # use std::process::Command;
/// # use std::process::Output;
/// # use std::process::ExitStatus;
/// # use command_error::Utf8ProgramAndArgs;
/// # use command_error::CommandDisplay;
/// # use command_error::OutputError;
/// let mut command = Command::new("sh");
/// command.args(["-c", "echo puppy doggy"]);
/// let displayed: Utf8ProgramAndArgs = (&command).into();
/// let error = OutputError::new(
/// Box::new(displayed),
/// Box::new(Output {
/// status: ExitStatus::default(),
/// stdout: "puppy doggy\n".as_bytes().to_vec(),
/// stderr: Vec::new(),
/// })
/// );
/// assert_eq!(
/// error.to_string(),
/// indoc!(
/// "`sh` failed: exit status: 0
/// Command failed: `sh -c 'echo puppy doggy'`
/// Stdout:
/// puppy doggy"
/// ),
/// );
/// assert_eq!(
/// error.with_message(Box::new("no kitties found!")).to_string(),
/// indoc!(
/// "`sh` failed: no kitties found!
/// exit status: 0
/// Command failed: `sh -c 'echo puppy doggy'`
/// Stdout:
/// puppy doggy"
/// )
/// );
/// ```
pub struct OutputError {
/// The program and arguments that ran.
pub(crate) command: Box<dyn CommandDisplay>,
Expand All @@ -18,6 +65,29 @@ pub struct OutputError {
pub(crate) user_error: Option<Box<dyn DebugDisplay>>,
}

impl OutputError {
/// Construct a new [`OutputError`].
pub fn new(command: Box<dyn CommandDisplay>, output: Box<dyn OutputLike>) -> Self {
Self {
command,
output,
user_error: None,
}
}

/// Attach a user-defined message to this error.
pub fn with_message(mut self, message: Box<dyn DebugDisplay>) -> Self {
self.user_error = Some(message);
self
}

/// Remove a user-defined message from this error, if any.
pub fn without_message(mut self) -> Self {
self.user_error = None;
self
}
}

impl Debug for OutputError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OutputError")
Expand Down Expand Up @@ -83,21 +153,6 @@ impl Display for OutputError {

impl std::error::Error for OutputError {}

impl OutputError {
pub(crate) fn new(command: Box<dyn CommandDisplay>, output: Box<dyn OutputLike>) -> Self {
Self {
command,
output,
user_error: None,
}
}

pub(crate) fn with_user_error(mut self, user_error: Option<Box<dyn DebugDisplay>>) -> Self {
self.user_error = user_error;
self
}
}

fn write_indented(f: &mut std::fmt::Formatter<'_>, text: &str, indent: &str) -> std::fmt::Result {
let mut lines = text.lines();
if let Some(line) = lines.next() {
Expand Down

0 comments on commit e206a65

Please sign in to comment.