use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
Env,
Fs,
Process,
Request,
}
#[derive(Debug, Clone)]
pub struct Error {
description: String,
kind: ErrorKind,
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
}
impl Error {
pub fn description(&self) -> &str {
&self.description
}
#[cfg(feature = "process")]
pub fn command_output(&self) -> Option<&crate::CommandOutput> {
self.source_as()
}
#[cfg(feature = "request")]
pub fn request_output(&self) -> Option<&crate::RequestOutput> {
self.source_as()
}
pub fn env<T: std::fmt::Display>(description: T) -> Self {
Self::new(ErrorKind::Env, description)
}
pub fn fs<T: std::fmt::Display>(description: T) -> Self {
Self::new(ErrorKind::Fs, description)
}
pub fn process<T: std::fmt::Display>(description: T) -> Self {
Self::new(ErrorKind::Process, description)
}
pub fn request<T: std::fmt::Display>(description: T) -> Self {
Self::new(ErrorKind::Request, description)
}
pub fn kind(&self) -> ErrorKind {
self.kind
}
pub fn source_as<T: std::error::Error + 'static>(&self) -> Option<&T> {
self.source
.as_deref()
.and_then(|source| source.downcast_ref::<T>())
}
#[cfg_attr(
not(any(feature = "fs", feature = "process", feature = "request")),
allow(dead_code)
)]
pub(crate) fn with_source<E: std::error::Error + Send + Sync + 'static>(
mut self,
source: E,
) -> Self {
self.source = Some(Arc::new(source));
self
}
fn new<T: std::fmt::Display>(kind: ErrorKind, description: T) -> Self {
Self {
kind,
description: description.to_string(),
source: None,
}
}
}
impl std::fmt::Display for ErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ErrorKind::Env => write!(f, "Env"),
ErrorKind::Fs => write!(f, "Fs"),
ErrorKind::Process => write!(f, "Process"),
ErrorKind::Request => write!(f, "Request"),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "[{}] {}", self.kind(), self.description())
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_deref()
.map(|source| source as &(dyn std::error::Error + 'static))
}
}
#[cfg(test)]
mod tests {
use super::*;
mod error_kind {
use super::*;
mod std_fmt_display {
use super::*;
#[test]
fn fmt_should_display_variant() {
for (variant, displayed) in &[
(ErrorKind::Env, "Env"),
(ErrorKind::Fs, "Fs"),
(ErrorKind::Process, "Process"),
(ErrorKind::Request, "Request"),
] {
assert_eq!(format!("{}", variant), *displayed);
}
}
}
}
mod error {
use super::*;
mod description {
use super::*;
#[test]
fn description_should_return_error_message() {
let error = Error::fs("some error");
assert_eq!(error.description(), "some error");
}
}
#[cfg(feature = "process")]
mod command_output {
use super::*;
#[test]
fn command_output_should_return_output_source() {
let output = crate::CommandOutput {
status: Some(1),
..Default::default()
};
let error = Error::process("failed").with_source(output.clone());
assert_eq!(error.command_output(), Some(&output));
}
#[test]
fn command_output_without_output_source_should_return_none() {
let io = Error::process("failed").with_source(std::io::Error::other("io error"));
assert_eq!(io.command_output(), None);
assert_eq!(Error::process("failed").command_output(), None);
}
}
#[cfg(feature = "request")]
mod request_output {
use super::*;
#[test]
fn request_output_should_return_output_source() {
let output = crate::RequestOutput {
status: 404,
..Default::default()
};
let error = Error::request("failed").with_source(output.clone());
assert_eq!(error.request_output(), Some(&output));
}
#[test]
fn request_output_without_output_source_should_return_none() {
assert_eq!(Error::request("failed").request_output(), None);
}
}
mod source_as {
use super::*;
#[test]
fn source_as_should_return_source_of_type() {
let error = Error::process("failed").with_source(std::io::Error::other("io error"));
assert_eq!(
error.source_as::<std::io::Error>().map(|e| e.to_string()),
Some("io error".to_string())
);
}
#[test]
fn source_as_without_source_should_return_none() {
assert!(
Error::process("failed")
.source_as::<std::io::Error>()
.is_none()
);
}
}
mod env {
use super::*;
#[test]
fn env_should_create_error_with_env_kind() {
let error = Error::env("env error");
assert_eq!(error.kind(), ErrorKind::Env);
assert_eq!(error.description(), "env error");
}
}
mod fs {
use super::*;
#[test]
fn fs_should_create_error_with_fs_kind() {
let error = Error::fs("fs error");
assert_eq!(error.kind(), ErrorKind::Fs);
assert_eq!(error.description(), "fs error");
}
}
mod process {
use super::*;
#[test]
fn process_should_create_error_with_process_kind() {
let error = Error::process("process error");
assert_eq!(error.kind(), ErrorKind::Process);
assert_eq!(error.description(), "process error");
}
}
mod request {
use super::*;
#[test]
fn request_should_create_error_with_request_kind() {
let error = Error::request("request error");
assert_eq!(error.kind(), ErrorKind::Request);
assert_eq!(error.description(), "request error");
}
}
mod kind {
use super::*;
#[test]
fn kind_should_return_error_kind() {
assert_eq!(Error::env("e").kind(), ErrorKind::Env);
assert_eq!(Error::fs("e").kind(), ErrorKind::Fs);
assert_eq!(Error::process("e").kind(), ErrorKind::Process);
assert_eq!(Error::request("e").kind(), ErrorKind::Request);
}
}
mod with_source {
use super::*;
#[test]
fn with_source_should_keep_kind_and_description() {
let error = Error::fs("fs error").with_source(std::io::Error::other("io error"));
assert_eq!(error.kind(), ErrorKind::Fs);
assert_eq!(error.description(), "fs error");
}
}
mod std_error_error {
use super::*;
mod source {
use super::*;
use std::error::Error as _;
#[test]
fn source_should_return_recorded_error() {
let error = Error::fs("fs error")
.with_source(std::io::Error::from(std::io::ErrorKind::NotFound));
assert_eq!(
error
.source()
.and_then(|source| source.downcast_ref::<std::io::Error>())
.map(std::io::Error::kind),
Some(std::io::ErrorKind::NotFound)
);
}
#[test]
fn source_without_recorded_error_should_return_none() {
assert!(Error::fs("fs error").source().is_none());
}
}
}
mod std_fmt_display {
use super::*;
#[test]
fn fmt_should_include_kind_prefix_and_description() {
let error = Error::fs("some error");
assert_eq!(format!("{error}"), "[Fs] some error");
}
}
}
}