Error Handling
Table of Contents
1 The Error
Type
The Rust idiomatic approach to handle errors is to rely on the Result
type
provided by the standard library. The introduction of the ?
operator, which
allows to seamlessly shortcut a computation which may fails (of type A ->
Result<B, E>
) in case one of its “subcomputation” fails is really convenient.
We define a custom Error
enum, with the following variants.
ConfigurationParsing |
PathBuf, toml::de::Error |
There is a parsing error inside a configuration file |
ConfigurationNotFound |
There is no configuration file neither in current directory nor in any of its parents | |
ConfigurationRead |
std::io::Error |
There is a configuration file, but cleopatra cannot read it |
InitWorkingDirectory |
std::io::Error |
Cleopatra could not set-up its working directory |
UnknownSubcommand |
String |
Cleopatra has been called with an incorrect subcommand |
Anomaly |
String |
Something else happened, that was not supposed to |
All the functions of cleopatra
which may fails, either because it relies on
such a function (typically, if we interact with the filesystem), shall return a
Result<A, cleopatra::error::Error>
value.
The correct labeling of errors with a dedicated variant remains a work in
progress at the moment. When no suitable variant are available, the ideal action
is to add this variant in Error
. But in practice, we currently use the
Anomaly
variant as a catch-all, and there is even a dedicated trait to make it
more convenient to “raise” Anomaly
errors: Raise
pub trait Raise { type In; fn or_raise(self, msg : &str) -> Result<Self::In, Error>; }
The main idea of the Raise
trait is to seamlessy combine functions which may
fail (either with the Result<A, E>
for any E
or the Option<A>
type), and
to project them in the Result<A, Error>
type. We do that by “forgetting” the
exact reason why we failed, and just raising an Anomaly
with a message that is
hopefully clear for the user.
impl<T> Raise for Option<T> { type In = T; fn or_raise(self, msg : &str) -> Result<T, Error> { self.ok_or(Error::Anomaly(String::from(msg))) } } impl<T, E> Raise for Result<T, E> { type In = T; fn or_raise(self, msg : &str) -> Result<T, Error> { self.map_err(|_| Error::Anomaly(String::from(msg))) } }
2 Formatting Error Messages
To give to the users of cleopatra
some feedback about failing executions, we
introduce the Message
structure, whose fields are the following.
title |
String |
A quick summary of the error | |
description |
String |
A longer message which supposedly provides a solution for the user |
Besides, we add a message
method to Error
, with the following prototype.
pub fn message(self) -> Message
The implementation of message
is simply a case enumeration.
Error::ConfigurationParsing(path, err) => Message { title : format!("{:?} is not a valid cleopatra.toml file", path), description : format!("The following error was found: {}.", err) }, Error::ConfigurationNotFound => Message { title : "cleopatra could not file its configuration file".into(), description : "You need to add a valid `cleopatra.toml' file at the root of your project.".into() }, Error::ConfigurationRead(io) => Message { title : "cleopatra could not read its configuration file".into(), description : format!("Its attempt failed with the following error: {}.", io) }, Error::InitWorkingDirectory(io) => Message { title : "cleopatra could not set-up its working directory".into(), description : format!("The filesystem reported the following error: {}", io) }, Error::UnknownSubcommand(cmd) => Message { title : format!("cleopatra has been called with a unsupported subcommand"), description : format!(r#"`{}' is not a valid subcommand of cleopatra. You can use `cleopatra --help' to get the list of supported subcommands."#, cmd) }, Error::Anomaly(msg) => Message { title : String::from("An anomaly occured, you probably have found a bug."), description : msg },