4 releases (breaking)
0.4.0 | Mar 20, 2023 |
---|---|
0.3.0 | Mar 11, 2023 |
0.2.0 | Jan 9, 2023 |
0.1.0 | Jun 18, 2021 |
#910 in Rust patterns
127 downloads per month
28KB
178 lines
🎈 thisctx
A small crate works with thiserror to create errors with contexts, heavily inspired by snafu.
✍️ Examples
#[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))
}
⚙️ Attributes
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
will also be
checked to determine the source error type.
Option arguments
#[thisctx]
supports two syntaxes for passing arguments to an option:
- Put tokens directly in the parentheses, e.g.
#[thisctx(visibility(pub))]
- Use a string literal, e.g.
#[thisctx(visibility = "pub")]
, this 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 will lead an error.
Boolean options
You can omit the true
value in boolean options, e.g. #[thisctx(skip)]
is
equal to #[thisctx(skip(true))]
.
Reversed boolean options starts with no_
can also be used as a shortcut to
pass false
, e.g. #[thisctx(no_skip)]
is equal to #[thisctx(skip(false))]
.
Inherited options
An inherited option uses the value of its parent node if no value is provided, for example:
#[derive(WithContext)]
#[thisctx(skip)]
enum Error {
// This variant will be ignored since `skip=true` is inherited.
Io(#[source] std::io::Error),
// This variant will be processed.
#[thisctx(no_skip)]
ParseInt(#[source] std::num::ParseIntError),
}
An option of type T[]
will concatenate 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 guaranteed 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;
source
If a field has the #[source]
attribute or is named source
, the type of this
field will be assigned to IntoError::Source
and won'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())
}
}
error
If a variant is transparent (which has #[error(transparent)]
), the first field
(which should also be the only field) will be considered as the source field.
thisctx.attr
An option used to add extra 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 the above example can also be written as:
#[derive(WithContext)]
#[thisctx(derive(Debug))]
struct Error {
reason: String,
}
thisctx.generic
An option to disable generics of a generated node.
#[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();
thisctx.into
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();
thisctx.module
This option allows you 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))]
.
thisctx.skip
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;
thisctx.suffix
An option to add a suffix to the names of the generated context types.
By default, only struct
s will be added the builtin suffix Context
since the
generated type without a suffix will confict 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
will remove the suffix from the generated type.
thisctx.unit
In Rust, the parentheses are required to construct a tuple struct even if it's
empty. thisctx
will convert 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 disabled 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;
thisctx.visibility
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;
📝 Todo
-
Switch to Rust 2021. - MSRV v1.33
- Use derive macro instead.
- Add attributes to context types.
- Support transparent error.
- Support generics.
- Simplify the derive implementation.
- More documentation.
- More tests.
🚩 Minimal suppoted Rust version
All tests under tests/*
passed with rustc v1.33
, previous versions may not
compile.
⚖️ License
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.
Dependencies
~1.5MB
~35K SLoC