A little crate that works with thiserror to create errors with context, heavily inspired by snafu.
#[derive(Debug, Error, WithContext)]
pub enum Error {
#[error("I/O failed at '{1}'")]
Io(#[source] std::io::Error, PathBuf),
#[error(transparent)]
ParseInt(std::num::ParseIntError),
}
fn read_file(path: &Path) -> Result<String, Error> {
std::fs::read_to_string(path).context(Io(path))
}
You can use the #[thisctx]
attribute with the following options to customize
the expanded code:
Option | Type | Inherited | Container | Variant | Field |
---|---|---|---|---|---|
attr |
TokenStream[] |
β | β | β | β |
generic |
bool |
β | β | β | β |
into |
Type[] |
β | β | β | |
module |
bool | Ident |
β | |||
skip |
Ident |
β | β | β | |
suffix |
bool | Ident |
β | β | β | |
unit |
bool |
β | β | β | |
visibility |
Visibility |
β | β | β | β |
The #[source]
and #[error]
attributes defined in thiserror
are also
checked to determine the source error type.
#[thisctx]
supports two syntaxes for passing arguments to an option:
- Put tokens directly in parentheses, e.g.
#[thisctx(visibility(pub))]
- Use a string literal, e.g.
#[thisctx(visibility = "pub")]
, which is useful in older versions ofrustc
that don't support arbitrary tokens in non-macro attributes.
An option of type T[]
can occur multiple times in the same node, while other
types result in an error.
You can omit the true
value in boolean options, e.g. #[thisctx(skip)]
is
equal to #[thisctx(skip(true))]
.
Negative boolean options starting with no_
can also be used as a shortcut to
pass false
, e.g. #[thisctx(no_skip)]
is equal to #[thisctx(skip(false))]
.
An inherited option uses the value of its parent node if no value is specified, for example:
#[derive(WithContext)]
#[thisctx(skip)]
enum Error {
// This variant is ignored since `skip=true` is inherited.
Io(#[source] std::io::Error),
// This variant is processed.
#[thisctx(no_skip)]
ParseInt(#[source] std::num::ParseIntError),
}
An option of type T[]
concatenates arguments from its ancestors instead of
overriding them:
#[derive(WithContext)]
#[thisctx(attr(derive(Debug)))]
enum Error {
#[thisctx(attr(derive(Clone, Copy)))]
Io(#[source] std::io::Error),
ParseInt(#[source] std::num::ParseIntError),
}
Expanded example:
// The order of attributes (and other options) is determined by the order of inheritance.
// Attributes from the child node.
#[derive(Clone, Copy)]
// Attributes from the parent node.
#[derive(Debug)]
struct Io;
#[derive(Debug)]
struct ParseInt;
If a field has the #[source]
attribute or is named source
, the type of this
field is assigned to IntoError::Source
and doesn't appear in the generated
context types.
#[derive(WithContext)]
struct Error(#[source] std::io::Error, PathBuf);
Expanded example:
struct ErrorContext<T1 = PathBuf>(T1);
impl<T1> IntoError<Error> for ErrorContext<T1>
where
T1: Into<PathBuf>,
{
type Source = std::io::Error;
fn into_error(self, source: Self::Source) -> Error {
Error(source, self.0.into())
}
}
If a variant is transparent (which has #[error(transparent)]
), the first field
(which should also be the only field) is considered as the source field.
An option used to append addition attributes to a generated node.
#[derive(WithContext)]
#[thisctx(attr(derive(Debug)))]
struct Error {
reason: String,
}
Expanded example:
#[derive(Debug)]
struct ErrorContext<T1 = String> {
reason: T1,
}
thisctx
allows you to add some common attributes without attr(...)
,
including:
cfg
cfg_attr
derive
doc
This means that the preceding example can also be written as:
#[derive(WithContext)]
#[thisctx(derive(Debug))]
struct Error {
reason: String,
}
An option to remove generics from generated code.
#[derive(WithContext)]
struct Error {
reason: String,
#[thisctx(no_generic)]
path: PathBuf,
}
Expanded example:
struct ErrorContext<T1 = String> {
reason: T1,
path: PathBuf,
}
The generics provide a convenient way to construct context types, for example:
let _: Error = ErrorContext {
// You can use &str directly because String implements From<&str>,
reason: "anyhow",
// whereas without generics you have to convert the data to PathBuf manually.
path: "/some/path".into(),
}.build();
An option for converting generated types to a remote error type.
// Probably an error defined in another crate.
enum RemoteError {
Custom(String),
}
// From<T> is required by #[thisctx(into)]
impl From<MyError> for RemoteError {
fn from(e: MyError) -> Self {
Self::Custom(e.0)
}
}
#[derive(WithContext)]
#[thisctx(into(RemoteError))]
struct MyError(String);
let _: MyError = MyErrorContext("anyhow").build();
// It's possible to construct a remote error from the local context type.
let _: RemoteError = MyErrorContext("anyhow").build();
This option allows you to put all generated context types into a single module.
#[derive(WithContext)]
#[thisctx(module(context))]
pub enum Error {
Io(#[source] std::io::Error),
ParseInt(#[source] std::num::ParseIntError),
}
Expanded example:
pub mod context {
pub struct Io;
pub struct ParseInt;
}
You can also set this option to
true
to use the snake case of the container name as the module name, e.g.#[thisctx(module)]
onenum MyError
is equal to#[thisctx(module(my_error))]
.
This option is used to skip generating context types for the specified variant.
#[derive(WithContext)]
enum Error {
#[thisctx(skip)]
Io(#[source] std::io::Error),
ParseInt(#[source] std::num::ParseIntError),
}
Expanded example:
struct ParseInt;
An option to add a suffix to the names of the generated context types.
By default, only struct
s get the builtin suffix Context
since the generated
types without suffix conflict with the error type.
#[derive(WithContext)]
#[thisctx(suffix(Error))]
enum Error {
Io(#[source] std::io::Error),
ParseInt(#[source] std::num::ParseIntError),
}
Expanded example:
struct IoError;
struct ParseIntError;
The value
true
means to use the default suffixContext
and the valuefalse
removes the suffix from the generated type.
In Rust, parentheses are required to construct a tuple struct even if it's
empty. thisctx
converts an empty struct to a unit struct by default. This
allows you use the struct name to create a new context without having to add
parentheses each time and can be turned off by passing #[thisctx(no_unit)]
.
#[derive(WithContext)]
enum Error {
#[thisctx(no_unit)]
Io(#[source] std::io::Error),
ParseInt(#[source] std::num::ParseIntError),
}
Expanded example:
struct IoError();
struct ParseIntError;
This option is used to change the visibility of the generated types and fields
and can be written in shorthand as #[pub(...)]
.
#[derive(WithContext)]
#[thisctx(pub(crate))]
pub enum Error {
Io(#[source] std::io::Error),
ParseInt(#[source] std::num::ParseIntError),
}
Expanded example:
pub(crate) struct IoError;
pub(crate) struct ParseIntError;
-
Switch to Rust 2021. - MSRV v1.33
- Use derive macro instead.
- Add attributes to context types.
- Support transparent errors.
- Support generics.
- Simplify the derive implementation.
- More documentation.
- More tests.
All tests under tests/*
passed with rustc v1.33
, earlier versions may not
compile.
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.