Repository: zkat/miette (rust)

Issue:
Can't create report from boxed diagnostic (impl Diagnostic for Box<dyn Diagnostic [ + Send + Sync]>?)
Hi!

I noticed that it doesn't seem to be possible to construct a `Report` from a boxed dynamic diagnostic. This seems a bit unfortunate, and should be fixable if we implement `Diagnostic` for `Box<dyn Diagnostic>` and the `Send` and `Sync` variants.

What do you think? Happy to submit a patch for this.

Pull request:
Add Report::new_boxed + more fixups + tests
This patchset:
* Adds a `Report::new_boxed` method.
* Finishes up the forwarding `Diagnostic` impls for `BoxedError` and a couple of other structs.
* Implements `AsRef<dyn StdError>` for `Report` since it's useful for some purposes, including the tests in the next commit.
* Adds tests for all of the above.

Closes #212.

Patch:
diff --git a/src/eyreish/error.rs b/src/eyreish/error.rs
index a7ed3bd..6b0dc34 100644
--- a/src/eyreish/error.rs
+++ b/src/eyreish/error.rs
@@ -73,6 +73,18 @@ impl Report {
         Report::from_adhoc(message)
     }
 
+    /// Create a new error object from a boxed [`Diagnostic`].
+    ///
+    /// The boxed type must be thread safe and 'static, so that the `Report`
+    /// will be as well.
+    ///
+    /// Boxed `Diagnostic`s don't implement `Diagnostic` themselves due to trait coherence issues.
+    /// This method allows you to create a `Report` from a boxed `Diagnostic`.
+    #[cfg_attr(track_caller, track_caller)]
+    pub fn new_boxed(error: Box<dyn Diagnostic + Send + Sync + 'static>) -> Self {
+        Report::from_boxed(error)
+    }
+
     #[cfg_attr(track_caller, track_caller)]
     pub(crate) fn from_std<E>(error: E) -> Self
     where
@@ -779,6 +791,18 @@ impl AsRef<dyn Diagnostic> for Report {
     }
 }
 
+impl AsRef<dyn StdError + Send + Sync> for Report {
+    fn as_ref(&self) -> &(dyn StdError + Send + Sync + 'static) {
+        unsafe { ErrorImpl::error(self.inner.by_ref()) }
+    }
+}
+
+impl AsRef<dyn StdError> for Report {
+    fn as_ref(&self) -> &(dyn StdError + 'static) {
+        unsafe { ErrorImpl::error(self.inner.by_ref()) }
+    }
+}
+
 impl std::borrow::Borrow<dyn Diagnostic> for Report {
     fn borrow(&self) -> &(dyn Diagnostic + 'static) {
         self.as_ref()
diff --git a/src/eyreish/wrapper.rs b/src/eyreish/wrapper.rs
index a5e67db..91a5ef3 100644
--- a/src/eyreish/wrapper.rs
+++ b/src/eyreish/wrapper.rs
@@ -102,6 +102,10 @@ impl Diagnostic for BoxedError {
     fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
         self.0.related()
     }
+
+    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
+        self.0.diagnostic_source()
+    }
 }
 
 impl Debug for BoxedError {
@@ -116,7 +120,21 @@ impl Display for BoxedError {
     }
 }
 
-impl StdError for BoxedError {}
+impl StdError for BoxedError {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        self.0.source()
+    }
+
+    fn description(&self) -> &str {
+        #[allow(deprecated)]
+        self.0.description()
+    }
+
+    fn cause(&self) -> Option<&dyn StdError> {
+        #[allow(deprecated)]
+        self.0.cause()
+    }
+}
 
 pub(crate) struct WithSourceCode<E, C> {
     pub(crate) error: E,
@@ -151,6 +169,10 @@ impl<E: Diagnostic, C: SourceCode> Diagnostic for WithSourceCode<E, C> {
     fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
         self.error.related()
     }
+
+    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
+        self.error.diagnostic_source()
+    }
 }
 
 impl<C: SourceCode> Diagnostic for WithSourceCode<Report, C> {
@@ -181,6 +203,10 @@ impl<C: SourceCode> Diagnostic for WithSourceCode<Report, C> {
     fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
         self.error.related()
     }
+
+    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
+        self.error.diagnostic_source()
+    }
 }
 
 impl<E: Debug, C> Debug for WithSourceCode<E, C> {


Test patch:
diff --git a/tests/test_boxed.rs b/tests/test_boxed.rs
index af16db7..d6fa6f8 100644
--- a/tests/test_boxed.rs
+++ b/tests/test_boxed.rs
@@ -1,4 +1,4 @@
-use miette::{miette, Diagnostic, Report};
+use miette::{miette, Diagnostic, LabeledSpan, Report, SourceSpan};
 use std::error::Error as StdError;
 use std::io;
 use thiserror::Error;
@@ -43,8 +43,15 @@ fn test_boxed_thiserror() {
     let error = MyError {
         source: io::Error::new(io::ErrorKind::Other, "oh no!"),
     };
-    let error: Report = miette!(error);
-    assert_eq!("oh no!", error.source().unwrap().to_string());
+    let report: Report = miette!(error);
+    assert_eq!("oh no!", report.source().unwrap().to_string());
+
+    let error = MyError {
+        source: io::Error::new(io::ErrorKind::Other, "oh no!!!!"),
+    };
+    let error: Box<dyn Diagnostic + Send + Sync + 'static> = Box::new(error);
+    let report = Report::new_boxed(error);
+    assert_eq!("oh no!!!!", report.source().unwrap().to_string());
 }
 
 #[test]
@@ -54,6 +61,162 @@ fn test_boxed_miette() {
     assert_eq!("oh no!", error.source().unwrap().to_string());
 }
 
+#[derive(Debug)]
+struct CustomDiagnostic {
+    source: Option<Report>,
+    related: Vec<Box<dyn Diagnostic + Send + Sync>>,
+}
+
+impl CustomDiagnostic {
+    const DISPLAY: &'static str = "CustomDiagnostic display";
+    const DESCRIPTION: &'static str = "CustomDiagnostic description";
+    const CODE: &'static str = "A042";
+    const SEVERITY: miette::Severity = miette::Severity::Advice;
+    const HELP: &'static str = "CustomDiagnostic help";
+    const URL: &'static str = "https://custom-diagnostic-url";
+    const LABEL: &'static str = "CustomDiagnostic label";
+    const SOURCE_CODE: &'static str = "this-is-some-source-code";
+
+    fn new() -> Self {
+        Self {
+            source: None,
+            related: Vec::new(),
+        }
+    }
+
+    fn with_source<E: StdError + Send + Sync + 'static>(self, source: E) -> Self {
+        let source = miette!(source);
+        Self {
+            source: Some(source),
+            related: Vec::new(),
+        }
+    }
+
+    fn with_related<D: Diagnostic + Send + Sync + 'static>(mut self, diagnostic: D) -> Self {
+        self.related.push(Box::new(diagnostic));
+        self
+    }
+}
+
+impl std::fmt::Display for CustomDiagnostic {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(Self::DISPLAY)
+    }
+}
+
+impl StdError for CustomDiagnostic {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        self.source.as_ref().map(|source| source.as_ref())
+    }
+
+    fn description(&self) -> &str {
+        Self::DESCRIPTION
+    }
+
+    fn cause(&self) -> Option<&dyn StdError> {
+        self.source.as_ref().map(|source| source.as_ref())
+    }
+}
+
+impl Diagnostic for CustomDiagnostic {
+    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+        Some(Box::new(Self::CODE))
+    }
+
+    fn severity(&self) -> Option<miette::Severity> {
+        Some(miette::Severity::Advice)
+    }
+
+    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+        Some(Box::new(Self::HELP))
+    }
+
+    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
+        Some(Box::new(Self::URL))
+    }
+
+    fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + 'a>> {
+        let labels = miette::LabeledSpan::new(Some(Self::LABEL.to_owned()), 0, 7);
+        Some(Box::new(std::iter::once(labels)))
+    }
+
+    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
+        Some(&Self::SOURCE_CODE)
+    }
+
+    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
+        Some(Box::new(
+            self.related.iter().map(|d| &**d as &'a dyn Diagnostic),
+        ))
+    }
+
+    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
+        self.source
+            .as_ref()
+            .map(|source| &**source as &dyn Diagnostic)
+    }
+}
+
+#[test]
+fn test_boxed_custom_diagnostic() {
+    fn assert_report(report: &Report) {
+        assert_eq!(
+            report.source().map(|source| source.to_string()),
+            Some("oh no!".to_owned()),
+        );
+        assert_eq!(
+            report.code().map(|code| code.to_string()),
+            Some(CustomDiagnostic::CODE.to_owned())
+        );
+        assert_eq!(report.severity(), Some(CustomDiagnostic::SEVERITY));
+        assert_eq!(
+            report.help().map(|help| help.to_string()),
+            Some(CustomDiagnostic::HELP.to_owned())
+        );
+        assert_eq!(
+            report.url().map(|url| url.to_string()),
+            Some(CustomDiagnostic::URL.to_owned())
+        );
+        assert_eq!(
+            report.labels().map(|labels| labels.collect::<Vec<_>>()),
+            Some(vec![LabeledSpan::new(
+                Some(CustomDiagnostic::LABEL.to_owned()),
+                0,
+                7
+            )]),
+        );
+        let span = SourceSpan::from(0..CustomDiagnostic::SOURCE_CODE.len());
+        assert_eq!(
+            report.source_code().map(|source_code| source_code
+                .read_span(&span, 0, 0)
+                .expect("read data from source code successfully")
+                .data()
+                .to_owned()),
+            Some(CustomDiagnostic::SOURCE_CODE.to_owned().into_bytes())
+        );
+        assert_eq!(
+            report.diagnostic_source().map(|source| source.to_string()),
+            Some("oh no!".to_owned()),
+        );
+    }
+
+    let related = CustomDiagnostic::new();
+    let main_diagnostic = CustomDiagnostic::new()
+        .with_source(io::Error::new(io::ErrorKind::Other, "oh no!"))
+        .with_related(related);
+
+    let report = Report::new_boxed(Box::new(main_diagnostic));
+    assert_report(&report);
+
+    let related = CustomDiagnostic::new();
+    let main_diagnostic = CustomDiagnostic::new()
+        .with_source(io::Error::new(io::ErrorKind::Other, "oh no!"))
+        .with_related(related);
+    let main_diagnostic = Box::new(main_diagnostic) as Box<dyn Diagnostic + Send + Sync + 'static>;
+    let report = miette!(main_diagnostic);
+    assert_report(&report);
+}
+
 #[test]
 #[ignore = "I don't know why this isn't working but it needs fixing."]
 fn test_boxed_sources() {