Skip to content

Commit

Permalink
feat(report): anyhow-ify DiagnosticReport (#35)
Browse files Browse the repository at this point in the history
This PR overhauls the toplevel/main experience for `miette`. It adds a new `Report` type based on `eyre::Report` and overhauls various types to fit into this model, as well as prepare for some [future changes in Rust](nrc/rfcs#1) that will make it possible to integrate `miette` directly with crates like `eyre` instead of having to use this specific `Report`.

As such, this PR is a major breaking change, especially for anyone using `DiagnosticReport` and company.

BREAKING CHANGES:
* `DiagnosticReport` is now just `Report`, and is a different, `eyre::Report`-like type.
* `DiagnosticResult` is now just `Result`.
* `.into_diagnostic()` now just transforms the error into a `Report`.
* `DiagnosticReportPrinter` has been replaced with `ReportHandler`
* `set_printer` has been replaced by `set_hook`
* `code` is now optional.
* `.into_diagnostic()` no longer takes a `code` argument.
* `#[diagnostic]` is now optional when deriving `Diagnostic`.
  • Loading branch information
zkat committed Sep 5, 2021
1 parent 0427c9f commit 3f9da04
Show file tree
Hide file tree
Showing 43 changed files with 2,884 additions and 313 deletions.
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exclude = ["images/", "tests/", "miette-derive/"]

[dependencies]
thiserror = "1.0.26"
miette-derive = { version = "=1.1.0", path = "miette-derive" }
miette-derive = { path = "miette-derive", version = "=1.1.0" }
once_cell = "1.8.0"
owo-colors = "2.0.0"
atty = "0.2.14"
Expand All @@ -22,5 +22,12 @@ ci_info = "0.14.2"
[dev-dependencies]
semver = "1.0.4"

# Eyre devdeps
futures = { version = "0.3", default-features = false }
indenter = "0.3.0"
rustversion = "1.0"
trybuild = { version = "1.0.19", features = ["diff"] }
syn = { version = "1.0", features = ["full"] }

[workspace]
members = ["miette-derive"]
2 changes: 1 addition & 1 deletion Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ args = ["--prepend", "CHANGELOG.md", "-u", "--tag", "${@}"]
workspace=false
install_crate="cargo-release"
command = "cargo"
args = ["release", "--tag-prefix", "", "--workspace", "${@}"]
args = ["release", "--workspace", "${@}"]
57 changes: 37 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ diagnostic error code: ruget::api::bad_json
- Unique error codes on every [Diagnostic].
- Custom links to get more details on error codes.
- Super handy derive macro for defining diagnostic metadata.
- Lightweight [`anyhow`](https://docs.rs/anyhow)/[`eyre`](https://docs.rs/eyre)-style error wrapper type, [DiagnosticReport],
- [`anyhow`](https://docs.rs/anyhow)/[`eyre`](https://docs.rs/eyre)-compatible error wrapper type, [Report],
which can be returned from `main`.
- Generic support for arbitrary [Source]s for snippet data, with default support for `String`s included.

The `miette` crate also comes bundled with a default [DiagnosticReportPrinter] with the following features:
The `miette` crate also comes bundled with a default [ReportHandler] with the following features:

- Fancy graphical [diagnostic output](#about), using ANSI/Unicode text
- single- and multi-line highlighting support
Expand Down Expand Up @@ -97,12 +97,12 @@ struct MyBad {
/*
Now let's define a function!
Use this DiagnosticResult type (or its expanded version) as the return type
Use this Result type (or its expanded version) as the return type
throughout your app (but NOT your libraries! Those should always return concrete
types!).
*/
use miette::{DiagnosticResult, NamedSource};
fn this_fails() -> DiagnosticResult<()> {
use miette::{Result, NamedSource};
fn this_fails() -> Result<()> {
// You can use plain strings as a `Source`, or anything that implements
// the one-method `Source` trait.
let src = "source\n text\n here".to_string();
Expand All @@ -118,12 +118,12 @@ fn this_fails() -> DiagnosticResult<()> {
}

/*
Now to get everything printed nicely, just return a DiagnosticResult<()>
Now to get everything printed nicely, just return a Result<()>
and you're all set!
Note: You can swap out the default reporter for a custom one using `miette::set_reporter()`
*/
fn pretend_this_is_main() -> DiagnosticResult<()> {
fn pretend_this_is_main() -> Result<()> {
// kaboom~
this_fails()?;

Expand Down Expand Up @@ -183,41 +183,58 @@ pub enum MyLibError {

Then, return this error type from all your fallible public APIs. It's a best
practice to wrap any "external" error types in your error `enum` instead of
using something like [eyre](https://docs.rs/eyre) in a library.
using something like [Report] in a library.

### ... in application code

Application code tends to work a little differently than libraries. You don't
always need or care to define dedicated error wrappers for errors coming from
external libraries and tools.

For this situation, `miette` includes two tools: [DiagnosticReport] and
For this situation, `miette` includes two tools: [Report] and
[IntoDiagnostic]. They work in tandem to make it easy to convert regular
`std::error::Error`s into [Diagnostic]s. Additionally, there's a
[DiagnosticResult] type alias that you can use to be more terse:
[Result] type alias that you can use to be more terse.

When dealing with non-`Diagnostic` types, you'll want to `.into_diagnostic()`
them:

```rust
// my_app/lib/my_internal_file.rs
use miette::{IntoDiagnostic, Result};
use semver::Version;

pub fn some_tool() -> Result<Version> {
Ok("1.2.x".parse().into_diagnostic()?)
}
```

`miette` also includes an `anyhow`/`eyre`-style `Context`/`WrapErr` traits that
you can import to add ad-hoc context messages to your `Diagnostic`s, as well,
though you'll still need to use `.into_diagnostic()` to make use of it:

```rust
// my_app/lib/my_internal_file.rs
use miette::{IntoDiagnostic, DiagnosticResult};
use miette::{IntoDiagnostic, Result, WrapErr};
use semver::Version;

pub fn some_tool() -> DiagnosticResult<Version> {
Ok("1.2.x".parse().into_diagnostic("my_app::semver::parse_error")?)
pub fn some_tool() -> Result<Version> {
Ok("1.2.x".parse().into_diagnostic().wrap_err("Parsing this tool's semver version failed.")?)
}
```

### ... in `main()`

`main()` is just like any other part of your application-internal code. Use
`DiagnosticResult` as your return value, and it will pretty-print your
`Result` as your return value, and it will pretty-print your
diagnostics automatically.

```rust
use miette::{DiagnosticResult, IntoDiagnostic};
use miette::{Result, IntoDiagnostic};
use semver::Version;

fn pretend_this_is_main() -> DiagnosticResult<()> {
let version: Version = "1.2.x".parse().into_diagnostic("my_app::semver::parse_error")?;
fn pretend_this_is_main() -> Result<()> {
let version: Version = "1.2.x".parse().into_diagnostic()?;
println!("{}", version);
Ok(())
}
Expand Down Expand Up @@ -249,7 +266,7 @@ use thiserror::Error;
#[diagnostic(
code(my_app::my_error),
// You can do formatting!
url("https://my_website.com/error_codes#{}", self.code())
url("https://my_website.com/error_codes#{}", self.code().unwrap())
)]
struct MyErr;
```
Expand Down Expand Up @@ -330,7 +347,7 @@ pub struct MyErrorType {
[`color-eyre`](https://crates.io/crates/color-eyre): these two enormously
influential error handling libraries have pushed forward the experience of
application-level error handling and error reporting. `miette`'s
`DiagnosticReport` type is an attempt at a very very rough version of their
`Report` type is an attempt at a very very rough version of their
`Report` types.
- [`thiserror`](https://crates.io/crates/thiserror) for setting the standard
for library-level error definitions, and for being the inspiration behind
Expand All @@ -344,7 +361,7 @@ pub struct MyErrorType {

`miette` is released to the Rust community under the [Apache license 2.0](./LICENSE).

It also includes some code taken from [`eyre`](https://github.com/yaahc/eyre),
It also includes code taken from [`eyre`](https://github.com/yaahc/eyre),
and some from [`thiserror`](https://github.com/dtolnay/thiserror), also under
the Apache License. Some code is taken from
[`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
21 changes: 11 additions & 10 deletions miette-derive/src/code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,30 @@ impl Code {
}| {
match args {
DiagnosticDefArgs::Transparent => {
forward_to_single_field_variant(ident, fields, quote! { code() })
Some(forward_to_single_field_variant(ident, fields, quote! { code() }))
}
DiagnosticDefArgs::Concrete(DiagnosticConcreteArgs { code, .. }) => {
let code = &code.0;
match fields {
let code = &code.as_ref()?.0;
Some(match fields {
syn::Fields::Named(_) => {
quote! { Self::#ident { .. } => std::boxed::Box::new(#code), }
quote! { Self::#ident { .. } => std::option::Option::Some(std::boxed::Box::new(#code)), }
}
syn::Fields::Unnamed(_) => {
quote! { Self::#ident(..) => std::boxed::Box::new(#code), }
quote! { Self::#ident(..) => std::option::Option::Some(std::boxed::Box::new(#code)), }
}
syn::Fields::Unit => {
quote! { Self::#ident => std::boxed::Box::new(#code), }
quote! { Self::#ident => std::option::Option::Some(std::boxed::Box::new(#code)), }
}
}
})
}
}
},
);
Some(quote! {
fn code<'a>(&'a self) -> std::boxed::Box<dyn std::fmt::Display + 'a> {
fn code<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
match self {
#(#code_pairs)*
_ => std::option::Option::None,
}
}
})
Expand All @@ -87,8 +88,8 @@ impl Code {
pub(crate) fn gen_struct(&self) -> Option<TokenStream> {
let code = &self.0;
Some(quote! {
fn code<'a>(&'a self) -> std::boxed::Box<dyn std::fmt::Display + 'a> {
std::boxed::Box::new(#code)
fn code<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
std::option::Option::Some(std::boxed::Box::new(#code))
}
})
}
Expand Down
23 changes: 12 additions & 11 deletions miette-derive/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ pub enum DiagnosticDefArgs {
Concrete(DiagnosticConcreteArgs),
}

#[derive(Default)]
pub struct DiagnosticConcreteArgs {
pub code: Code,
pub code: Option<Code>,
pub severity: Option<Severity>,
pub help: Option<Help>,
pub snippets: Option<Snippets>,
Expand All @@ -44,7 +45,7 @@ pub struct DiagnosticConcreteArgs {

impl DiagnosticConcreteArgs {
fn parse(
ident: &syn::Ident,
_ident: &syn::Ident,
fields: &syn::Fields,
attr: &syn::Attribute,
args: impl Iterator<Item = DiagnosticArg>,
Expand Down Expand Up @@ -75,8 +76,7 @@ impl DiagnosticConcreteArgs {
}
let snippets = Snippets::from_fields(fields)?;
let concrete = DiagnosticConcreteArgs {
code: code
.ok_or_else(|| syn::Error::new(ident.span(), "Diagnostic code is required."))?,
code,
help,
severity,
snippets,
Expand Down Expand Up @@ -132,11 +132,12 @@ impl Diagnostic {
args,
}
} else {
// Also handle when there's multiple `#[diagnostic]` attrs?
return Err(syn::Error::new(
input.ident.span(),
"#[diagnostic] attribute is required when deriving Diagnostic.",
));
Diagnostic::Struct {
fields: data_struct.fields,
ident: input.ident,
generics: input.generics,
args: DiagnosticDefArgs::Concrete(Default::default()),
}
}
}
syn::Data::Enum(syn::DataEnum { variants, .. }) => {
Expand Down Expand Up @@ -206,7 +207,7 @@ impl Diagnostic {

quote! {
impl #impl_generics miette::Diagnostic for #ident #ty_generics #where_clause {
fn code<'a>(&'a self) -> std::boxed::Box<dyn std::fmt::Display + 'a> {
fn code<'a>(&'a self) -> std::option::Option<std::boxed::Box<dyn std::fmt::Display + 'a>> {
#matcher
#field_name.code()
}
Expand All @@ -230,7 +231,7 @@ impl Diagnostic {
}
}
DiagnosticDefArgs::Concrete(concrete) => {
let code_body = concrete.code.gen_struct();
let code_body = concrete.code.as_ref().and_then(|x| x.gen_struct());
let help_body = concrete.help.as_ref().and_then(|x| x.gen_struct(fields));
let sev_body = concrete.severity.as_ref().and_then(|x| x.gen_struct());
let snip_body = concrete
Expand Down
2 changes: 1 addition & 1 deletion miette-derive/src/severity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ impl Severity {
fn severity(&self) -> std::option::Option<miette::Severity> {
match self {
#(#sev_pairs)*
_ => None,
_ => std::option::Option::None,
}
}
})
Expand Down
4 changes: 2 additions & 2 deletions src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use ChainState::*;

#[derive(Clone)]
#[allow(missing_debug_implementations)]
pub(crate) struct Chain<'a> {
pub struct Chain<'a> {
state: crate::chain::ChainState<'a>,
}

Expand All @@ -25,7 +25,7 @@ pub(crate) enum ChainState<'a> {
}

impl<'a> Chain<'a> {
pub(crate) fn new(head: &'a (dyn StdError + 'static)) -> Self {
pub fn new(head: &'a (dyn StdError + 'static)) -> Self {
Chain {
state: ChainState::Linked { next: Some(head) },
}
Expand Down
4 changes: 2 additions & 2 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ pub enum MietteError {
)]
OutOfBounds,

/// Returned when installing a [crate::DiagnosticReportPrinter] failed.
/// Returned when installing a [crate::ReportHandler] failed.
/// Typically, this will be because [crate::set_printer] was called twice.
#[error("Failed to install DiagnosticReportPrinter")]
#[error("Failed to install ReportHandler")]
#[diagnostic(code(miette::set_printer_failed), url(docsrs))]
SetPrinterFailure,
}

0 comments on commit 3f9da04

Please sign in to comment.