use std::fmt;
use crate::name::InvalidName;
pub type Outcome = Result<(), Failure>;
#[derive(Debug)]
pub struct Failure {
message: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl Failure {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
#[must_use]
pub fn caused_by(mut self, cause: impl std::error::Error + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(cause));
self
}
}
impl fmt::Display for Failure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.source {
Some(cause) => write!(formatter, "{}: {cause}", self.message),
None => formatter.write_str(&self.message),
}
}
}
impl std::error::Error for Failure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_deref()
.map(|error| error as &(dyn std::error::Error + 'static))
}
}
impl From<InvalidName> for Failure {
fn from(error: InvalidName) -> Self {
Self::new(error.to_string())
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use super::Failure;
use crate::name::Name;
#[test]
fn display_is_the_message() {
let failure = Failure::new("tasks/lint already exists");
assert_eq!(failure.to_string(), "tasks/lint already exists");
}
#[test]
fn source_is_absent_until_caused_by_is_called() {
let failure = Failure::new("something went wrong");
assert!(failure.source().is_none());
}
#[test]
fn source_is_the_cause_after_caused_by() {
#[derive(Debug)]
struct Cause;
impl std::fmt::Display for Cause {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("the underlying cause")
}
}
impl Error for Cause {}
let failure = Failure::new("writing failed").caused_by(Cause);
let source = failure.source();
assert!(source.is_some(), "a cause was attached");
if let Some(source) = source {
assert_eq!(source.to_string(), "the underlying cause");
}
}
#[test]
fn display_carries_the_cause_when_one_is_attached_and_omits_it_otherwise() {
#[derive(Debug)]
struct Cause;
impl std::fmt::Display for Cause {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("the underlying cause")
}
}
impl Error for Cause {}
let caused = Failure::new("writing failed").caused_by(Cause);
assert_eq!(caused.to_string(), "writing failed: the underlying cause");
let plain = Failure::new("writing failed");
assert_eq!(plain.to_string(), "writing failed");
}
#[test]
fn from_invalid_name_keeps_the_message_verbatim() {
assert!(Name::new("../evil").is_err(), "`../evil` must be refused");
if let Err(invalid_name) = Name::new("../evil") {
let expected = invalid_name.to_string();
let failure = Failure::from(invalid_name);
assert_eq!(failure.to_string(), expected);
}
}
}