Repository: zkat/miette (rust)

Issue:
Implement `WrapError` for `Option`
`anyhow` [implements it's `Context` trait on options](https://docs.rs/anyhow/latest/anyhow/trait.Context.html#impl-Context%3CT,+Infallible%3E-for-Option%3CT%3E). This helps avoid some boilerplate:

```rust
use anyhow::Context;

let map = HashMap::from([("a", 1), ("b", 2), ("c", 3)]);
let _ = map.get("d").with_context(|| format!("Key 'd' was missing from map: {map:?}"))?;
```

Currently the best way to do this with `miette` is by using a `let else` then `bail!`:

```rust
use miette::bail;

let map = HashMap::from([("a", 1), ("b", 2), ("c", 3)]);
let Some(_) = map.get("d") else {
    bail!("Key 'd' was missing from map: {map:?}"));
};
```

I'm willing to implement this if desired

Pull request:
feat: Implement `WrapError` for `Option`
Implement `WrapError` for `Option<T>`. This is inline with `anyhow` that also implements `Context` for `Option<T>`.

The implementation requires us to introduce a `DisplayError` internal only type, that creates an error from a type that only implements `Display` (`Report::from_adhoc` requires the type to also implement `Debug`, but `WrapError` only requires it to implement `Display`).

For this I copied `MessageError` and adapted it to implement `Debug` using the underlying type's `Display` impl. This is a bit of a hack, but anyhow does [something similar][1].

Closes #408

[1]: https://docs.rs/anyhow/latest/src/anyhow/wrapper.rs.html#34

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b179baa..5641367 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,38 @@
 # `miette` Release Changelog
 
+<a name="7.3.0"></a>
+## 7.3.0 (2024-11-26)
+
+### Features
+
+* **SourceSpan:** add impl From<InclusiveRange> (#385) ([73da45b6](https://github.com/zkat/miette/commit/73da45b65c965777a00ba64aa03a247c0e5241ca))
+* **Report:** add `from_err()` method to `Report` (#403) ([93d3bd11](https://github.com/zkat/miette/commit/93d3bd118a072c35aa761f0ec74317166ec08113))
+* **Diagnostic:** Implement `Diagnostic` for `Infallible` (#402) ([f3fb4c1e](https://github.com/zkat/miette/commit/f3fb4c1ecd196ce389cbd71139bb7e3b35474add))
+
+### Performance
+
+* **handlers:** optimize string-buffer reallocations (#387) ([b8dfcda4](https://github.com/zkat/miette/commit/b8dfcda4a8c10a14116ee275250356ac991dc7be))
+
+### Bug Fixes
+
+* **graphical:** fix nested error wrapping (#358) ([3eabbceb](https://github.com/zkat/miette/commit/3eabbcebf113d1d620a6a3f98e8a455414ed3042))
+* **docs:** updated example image (fixes #111) (#270) ([7b42b12c](https://github.com/zkat/miette/commit/7b42b12c5f6316322ce79c59bcb9e99f5d49edb8))
+* **clippy:** Fix clippy lints in docs (#365) ([ea4296da](https://github.com/zkat/miette/commit/ea4296dacec3b0e4762281d9d115c1bd69ecfac3))
+* **docs:** `alt` attribut for `single-line-example.png` (#372) ([b82cc81b](https://github.com/zkat/miette/commit/b82cc81b8ea32a1cf1b4598ed5832bc8e3b0e161))
+* **color:** setting NO_COLOR should not print ansi codes for non-terminals (#381) ([813232ba](https://github.com/zkat/miette/commit/813232ba7957ae09e4fb9d9416d821f4fd9da66d))
+* **clippy:** fix Rust v1.78.0 clippy warnings (#382) ([e1026f75](https://github.com/zkat/miette/commit/e1026f75e0a5d19bbc8e468cb3f5292074543a97))
+* **perf:** mark error constructors cold (#378) ([9bbcf3c6](https://github.com/zkat/miette/commit/9bbcf3c6017fa3455a7db714879816c1cfc511fd))
+* **handlers:** Disable textwrap::smawk feature (#379) ([edfdcb52](https://github.com/zkat/miette/commit/edfdcb525ee30fc54747460ada621f13f0ed1996))
+* **graphical:** Format entire link instead of just name (#389) ([bf5aa374](https://github.com/zkat/miette/commit/bf5aa3742fd664be3c93160b9c28c145b1ed8bc9))
+* **clippy:** fix `clippy::doc_lazy_continuation` lints (#395) ([15beec43](https://github.com/zkat/miette/commit/15beec43303180b811d4c04d1a78775feb9b0905))
+* **graphical:** Handle invalid UTF-8 in source code (#393) ([d6b45585](https://github.com/zkat/miette/commit/d6b4558502e82fa74e030ccb3c8040656590d7eb))
+* **features:** Use `dep:` syntax for dependencies in features. (#394) ([789a04e3](https://github.com/zkat/miette/commit/789a04e30d041179b373b4eb8b340456534a3f0e))
+* **clippy:** Fix `needless_return` lint. (#405) ([5f441d01](https://github.com/zkat/miette/commit/5f441d011560a091fe5d6a6cdb05f09acf622d36))
+
+### Documentation
+
+* **examples:** add serde_json integration example (#407) ([2902a233](https://github.com/zkat/miette/commit/2902a2337c2e36a5d8e0e54b007d6100cca0c9ff))
+
 <a name="7.2.0"></a>
 ## 7.2.0 (2024-03-07)
 
diff --git a/Cargo.toml b/Cargo.toml
index 70d7859..ee00a05 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "miette"
-version = "7.2.0"
+version = "7.3.0"
 authors = ["Kat Marchán <kzm@zkat.tech>"]
 description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers."
 categories = ["rust-patterns"]
@@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"]
 
 [dependencies]
 thiserror = "1.0.56"
-miette-derive = { path = "miette-derive", version = "=7.2.0", optional = true }
+miette-derive = { path = "miette-derive", version = "=7.3.0", optional = true }
 unicode-width = "0.1.11"
 cfg-if = "1.0.0"
 
diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml
index e8082a0..fb05f89 100644
--- a/miette-derive/Cargo.toml
+++ b/miette-derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "miette-derive"
-version = "7.2.0"
+version = "7.3.0"
 authors = ["Kat Marchán <kzm@zkat.tech>"]
 edition = "2018"
 license = "Apache-2.0"
diff --git a/src/eyreish/context.rs b/src/eyreish/context.rs
index 3d9238b..ee1fa08 100644
--- a/src/eyreish/context.rs
+++ b/src/eyreish/context.rs
@@ -38,6 +38,44 @@ mod ext {
     }
 }
 
+impl<T> WrapErr<T, std::convert::Infallible> for Option<T> {
+    fn wrap_err<D>(self, msg: D) -> Result<T, Report>
+    where
+        D: Display + Send + Sync + 'static,
+    {
+        match self {
+            Some(t) => Ok(t),
+            None => Err(Report::from(crate::eyreish::wrapper::DisplayError(msg))),
+        }
+    }
+
+    fn wrap_err_with<D, F>(self, msg: F) -> Result<T, Report>
+    where
+        D: Display + Send + Sync + 'static,
+        F: FnOnce() -> D,
+    {
+        match self {
+            Some(t) => Ok(t),
+            None => Err(Report::from(crate::eyreish::wrapper::DisplayError(msg()))),
+        }
+    }
+
+    fn context<D>(self, msg: D) -> Result<T, Report>
+    where
+        D: Display + Send + Sync + 'static,
+    {
+        self.wrap_err(msg)
+    }
+
+    fn with_context<D, F>(self, msg: F) -> Result<T, Report>
+    where
+        D: Display + Send + Sync + 'static,
+        F: FnOnce() -> D,
+    {
+        self.wrap_err_with(msg)
+    }
+}
+
 impl<T, E> WrapErr<T, E> for Result<T, E>
 where
     E: ext::Diag + Send + Sync + 'static,
diff --git a/src/eyreish/wrapper.rs b/src/eyreish/wrapper.rs
index 62cf7a4..bab7320 100644
--- a/src/eyreish/wrapper.rs
+++ b/src/eyreish/wrapper.rs
@@ -6,6 +6,30 @@ use crate::{Diagnostic, LabeledSpan, Report, SourceCode};
 
 use crate as miette;
 
+#[repr(transparent)]
+pub(crate) struct DisplayError<M>(pub(crate) M);
+
+impl<M> Debug for DisplayError<M>
+where
+    M: Display,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Display::fmt(&self.0, f)
+    }
+}
+
+impl<M> Display for DisplayError<M>
+where
+    M: Display,
+{
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Display::fmt(&self.0, f)
+    }
+}
+
+impl<M> StdError for DisplayError<M> where M: Display + 'static {}
+impl<M> Diagnostic for DisplayError<M> where M: Display + 'static {}
+
 #[repr(transparent)]
 pub(crate) struct MessageError<M>(pub(crate) M);
 
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index 8c4a9a5..9e0948c 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -218,9 +218,18 @@ impl GraphicalReportHandler {
         f: &mut impl fmt::Write,
         diagnostic: &(dyn Diagnostic),
     ) -> fmt::Result {
+        self.render_report_inner(f, diagnostic, diagnostic.source_code())
+    }
+
+    fn render_report_inner(
+        &self,
+        f: &mut impl fmt::Write,
+        diagnostic: &(dyn Diagnostic),
+        parent_src: Option<&dyn SourceCode>,
+    ) -> fmt::Result {
+        let src = diagnostic.source_code().or(parent_src);
         self.render_header(f, diagnostic)?;
-        self.render_causes(f, diagnostic)?;
-        let src = diagnostic.source_code();
+        self.render_causes(f, diagnostic, src)?;
         self.render_snippets(f, diagnostic, src)?;
         self.render_footer(f, diagnostic)?;
         self.render_related(f, diagnostic, src)?;
@@ -278,7 +287,14 @@ impl GraphicalReportHandler {
         Ok(())
     }
 
-    fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
+    fn render_causes(
+        &self,
+        f: &mut impl fmt::Write,
+        diagnostic: &(dyn Diagnostic),
+        parent_src: Option<&dyn SourceCode>,
+    ) -> fmt::Result {
+        let src = diagnostic.source_code().or(parent_src);
+
         let (severity_style, severity_icon) = match diagnostic.severity() {
             Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
             Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
@@ -356,7 +372,7 @@ impl GraphicalReportHandler {
                         inner_renderer.with_cause_chain = false;
                         // Since everything from here on is indented, shrink the virtual terminal
                         inner_renderer.termwidth -= rest_indent.width();
-                        inner_renderer.render_report(&mut inner, diag)?;
+                        inner_renderer.render_report_inner(&mut inner, diag, src)?;
 
                         // If there was no header, remove the leading newline
                         let inner = inner.trim_start_matches('\n');
@@ -410,8 +426,8 @@ impl GraphicalReportHandler {
                     Some(Severity::Advice) => write!(f, "Advice: ")?,
                 };
                 inner_renderer.render_header(f, rel)?;
-                inner_renderer.render_causes(f, rel)?;
                 let src = rel.source_code().or(parent_src);
+                inner_renderer.render_causes(f, rel, src)?;
                 inner_renderer.render_snippets(f, rel, src)?;
                 inner_renderer.render_footer(f, rel)?;
                 inner_renderer.render_related(f, rel, src)?;
diff --git a/src/lib.rs b/src/lib.rs
index 3c1a6fe..175c929 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -100,7 +100,7 @@
 //!
 //! `thiserror` is a great way to define them, and plays nicely with `miette`!
 //! */
-//! use miette::{Diagnostic, SourceSpan};
+//! use miette::{Diagnostic, NamedSource, SourceSpan};
 //! use thiserror::Error;
 //!
 //! #[derive(Error, Debug, Diagnostic)]
@@ -127,7 +127,7 @@
 //! throughout your app (but NOT your libraries! Those should always return
 //! concrete types!).
 //! */
-//! use miette::{NamedSource, Result};
+//! use miette::Result;
 //! fn this_fails() -> Result<()> {
 //!     // You can use plain strings as a `Source`, or anything that implements
 //!     // the one-method `Source` trait.


Test patch:
diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs
index bbc2179..b21e091 100644
--- a/tests/test_diagnostic_source_macro.rs
+++ b/tests/test_diagnostic_source_macro.rs
@@ -299,3 +299,53 @@ fn test_nested_cause_chains_for_related_errors_are_output() {
 
     assert_eq!(expected, out);
 }
+
+#[cfg(feature = "fancy-no-backtrace")]
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+#[error("A case1 error happened")]
+enum NestedEnumError {
+    Case1 {
+        #[source_code]
+        code: String,
+        #[diagnostic_source]
+        the_other_err: Case1Inner,
+    },
+}
+
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+#[error("I am the inner error")]
+struct Case1Inner {
+    #[label("inner-label")]
+    label: (usize, usize),
+}
+
+#[cfg(feature = "fancy-no-backtrace")]
+#[test]
+fn source_is_inherited_to_causes() {
+    let diag = NestedEnumError::Case1 {
+        code: String::from("this is the parent source"),
+        the_other_err: Case1Inner { label: (8, 3) },
+    };
+    let mut out = String::new();
+    miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
+        .with_width(80)
+        .with_footer("Yooo, a footer".to_string())
+        .render_report(&mut out, &diag)
+        .unwrap();
+    println!("{}", out);
+
+    let expected = r#"
+  × A case1 error happened
+  ╰─▶   × I am the inner error
+         ╭────
+       1 │ this is the parent source
+         ·         ─┬─
+         ·          ╰── inner-label
+         ╰────
+      
+
+  Yooo, a footer
+"#;
+
+    assert_eq!(expected, out);
+}