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

#1083 in Rust patterns

44 downloads per month

MIT/Apache

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 of rustc 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)] on enum 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 structs 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 suffix Context and the value false 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

at your option.

Dependencies

~1.5MB
~37K SLoC