Repository: zkat/miette
Pull request #262 (2023-05-05 14:51:06)

Add dynamic diagnostic
Draft for dynamic diagnostics for design discussion. 
 
Implements #259 

gavrilikhin-d: Do you agree with `DynamicDiagnostic` name?
gavrilikhin-d: How generic should this class be? 

I tried using generic types with defaults:
```rs
struct DynamicDiagnostic<D = String, C = String, ...> 
where
    D: Debug + Display,
    C: Debug + Display,
    ...
{
   pub description: D,
   pub code: Option<C>,
   ...
}
```
But this was  ambiguous if using `None` values:
```rs
let diag: DynamicDiagnostic<&str, _, ...> = DynamicDiagnostic {
   description: "",
   code: None
};
```
gavrilikhin-d: is `cargo readme` from #124 not longer working?
zkat: > is `cargo readme` from #124 not longer working?

@gavrilikhin-d `cargo make readme`
zkat: looks like you have a clippy thing too
gavrilikhin-d: Issues #259 and #201 will be resolved by this pr
gavrilikhin-d: Why tests fail on windows? Looks like it can't find local arguments and assumes they are named arguments
zkat: I think 1.56.0 is before format argument interpolation was stabilized, now that I think about it
gavrilikhin-d: @zkat how can I fix it? See no way to conditionally compile code depending on rust version. 

My best guess is to introduce new default feature `format_args_capture`
gavrilikhin-d: Now it will pass workflow (checked in my fork)

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 710eb1c..4fafac3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -43,7 +43,11 @@ jobs:
       - name: Clippy
         run: cargo clippy --all -- -D warnings
       - name: Run tests
+        if: matrix.rust == 'stable'
         run: cargo test --all --verbose --features fancy
+      - name: Run tests
+        if: matrix.rust == '1.56.0'
+        run: cargo test --all --verbose --features fancy no-format-args-capture
 
   miri:
     name: Miri
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b38d029..87215d6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,7 +23,7 @@
 
 ## Introduction
 
-Thank you so much for your interest in contributing!. All types of contributions are encouraged and valued. See the [table of contents](#toc) for different ways to help and details about how this project handles them!📝
+Thank you so much for your interest in contributing! All types of contributions are encouraged and valued. See the [table of contents](#toc) for different ways to help and details about how this project handles them!📝
 
 Please make sure to read the relevant section before making your contribution! It will make it a lot easier for us maintainers to make the most of it and smooth out the experience for all involved. 💚
 
diff --git a/Cargo.toml b/Cargo.toml
index 7615d6b..61d357a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -42,7 +42,16 @@ lazy_static = "1.4"
 
 [features]
 default = []
-fancy-no-backtrace = ["owo-colors", "is-terminal", "textwrap", "terminal_size", "supports-hyperlinks", "supports-color", "supports-unicode"]
+no-format-args-capture = []
+fancy-no-backtrace = [
+    "owo-colors",
+    "is-terminal",
+    "textwrap",
+    "terminal_size",
+    "supports-hyperlinks",
+    "supports-color",
+    "supports-unicode",
+]
 fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"]
 
 [workspace]
diff --git a/README.md b/README.md
index c12734d..ff25b3a 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 You run miette? You run her code like the software? Oh. Oh! Error code for
 coder! Error code for One Thousand Lines!
 
-### About
+## About
 
 `miette` is a diagnostic library for Rust. It includes a series of
 traits/protocols that allow you to hook into its error reporting facilities,
@@ -32,7 +32,7 @@ output like in the screenshots above.** You should only do this in your
 toplevel crate, as the fancy feature pulls in a number of dependencies that
 libraries and such might not want.
 
-### Table of Contents <!-- omit in toc -->
+## Table of Contents <!-- omit in toc -->
 
 - [About](#about)
 - [Features](#features)
@@ -47,10 +47,11 @@ libraries and such might not want.
   - [... multiple related errors](#-multiple-related-errors)
   - [... delayed source code](#-delayed-source-code)
   - [... handler options](#-handler-options)
+  - [... dynamic diagnostics](#-dynamic-diagnostics)
 - [Acknowledgements](#acknowledgements)
 - [License](#license)
 
-### Features
+## Features
 
 - Generic [`Diagnostic`] protocol, compatible (and dependent on)
   [`std::error::Error`].
@@ -75,7 +76,7 @@ the following features:
 - Cause chain printing
 - Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
 
-### Installing
+## Installing
 
 ```sh
 $ cargo add miette
@@ -87,7 +88,7 @@ If you want to use the fancy printer in all these screenshots:
 $ cargo add miette --features fancy
 ```
 
-### Example
+## Example
 
 ```rust
 /*
@@ -169,9 +170,9 @@ diagnostic help: Change int or string to be the right types and try again.
 diagnostic code: nu::parser::unsupported_operation
 For more details, see https://docs.rs/nu-parser/0.1.0/nu-parser/enum.ParseError.html#variant.UnsupportedOperation">
 
-### Using
+## Using
 
-#### ... in libraries
+### ... in libraries
 
 `miette` is _fully compatible_ with library usage. Consumers who don't know
 about, or don't want, `miette` features can safely use its error types as
@@ -205,7 +206,7 @@ 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 [`Report`] in a library.
 
-#### ... in application code
+### ... 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
@@ -247,11 +248,11 @@ pub fn some_tool() -> Result<Version> {
 }
 ```
 
-To construct your own simple adhoc error use the `miette::miette!` macro:
+To construct your own simple adhoc error use the [`miette!`] macro:
 
 ```rust
 // my_app/lib/my_internal_file.rs
-use miette::{IntoDiagnostic, Result, WrapErr, miette};
+use miette::{miette, IntoDiagnostic, Result, WrapErr};
 use semver::Version;
 
 pub fn some_tool() -> Result<Version> {
@@ -262,9 +263,7 @@ pub fn some_tool() -> Result<Version> {
 }
 ```
 
-There are also similar `miette::bail!` and `miette::ensure!` macros.
-
-#### ... in `main()`
+### ... in `main()`
 
 `main()` is just like any other part of your application-internal code. Use
 `Result` as your return value, and it will pretty-print your diagnostics
@@ -294,7 +293,7 @@ enabled:
 miette = { version = "X.Y.Z", features = ["fancy"] }
 ```
 
-#### ... diagnostic code URLs
+### ... diagnostic code URLs
 
 `miette` supports providing a URL for individual diagnostics. This URL will
 be displayed as an actual link in supported terminals, like so:
@@ -347,7 +346,7 @@ use thiserror::Error;
 struct MyErr;
 ```
 
-#### ... snippets
+### ... snippets
 
 Along with its general error handling and reporting features, `miette` also
 includes facilities for adding error spans/annotations/labels to your
@@ -395,8 +394,7 @@ pub struct MyErrorType {
 }
 ```
 
-##### ... help text
-
+#### ... help text
 `miette` provides two facilities for supplying help text for your errors:
 
 The first is the `#[help()]` format attribute that applies to structs or
@@ -432,7 +430,7 @@ let err = Foo {
 };
 ```
 
-#### ... multiple related errors
+### ... multiple related errors
 
 `miette` supports collecting multiple errors into a single diagnostic, and
 printing them all together nicely.
@@ -452,7 +450,7 @@ struct MyError {
 }
 ```
 
-#### ... delayed source code
+### ... delayed source code
 
 Sometimes it makes sense to add source code to the error message later.
 One option is to use [`with_source_code()`](Report::with_source_code)
@@ -535,7 +533,7 @@ fn main() -> miette::Result<()> {
 }
 ```
 
-#### ... Diagnostic-based error sources.
+### ... Diagnostic-based error sources.
 
 When one uses the `#[source]` attribute on a field, that usually comes
 from `thiserror`, and implements a method for
@@ -568,7 +566,7 @@ struct MyError {
 struct OtherError;
 ```
 
-#### ... handler options
+### ... handler options
 
 [`MietteHandler`] is the default handler, and is very customizable. In
 most cases, you can simply use [`MietteHandlerOpts`] to tweak its behavior
@@ -587,13 +585,32 @@ miette::set_hook(Box::new(|_| {
             .build(),
     )
 }))
-
 ```
 
 See the docs for [`MietteHandlerOpts`] for more details on what you can
 customize!
 
-### Acknowledgements
+### ... dynamic diagnostics
+
+If you...
+- ...don't know all the possible errors upfront
+- ...need to serialize/deserialize errors
+then you may want to use [`miette!`], [`diagnostic!`] macros or
+[`MietteDiagnostic`] directly to create diagnostic on the fly.
+
+```rust
+let source = "2 + 2 * 2 = 8".to_string();
+let report = miette!(
+  labels = vec[
+      LabeledSpan::at(12..13, "this should be 6"),
+  ],
+  help = "'*' has greater precedence than '+'",
+  "Wrong answer"
+).with_source_code(source);
+println!("{:?}", report)
+```
+
+## Acknowledgements
 
 `miette` was not developed in a void. It owes enormous credit to various
 other projects and their authors:
@@ -612,7 +629,7 @@ other projects and their authors:
 - [`ariadne`](https://crates.io/crates/ariadne) for pushing forward how
   _pretty_ these diagnostics can really look!
 
-### License
+## License
 
 `miette` is released to the Rust community under the [Apache license
 2.0](./LICENSE).
@@ -623,11 +640,13 @@ under the Apache License. Some code is taken from
 [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
 
 [`miette!`]: https://docs.rs/miette/latest/miette/macro.miette.html
+[`diagnostic!`]: https://docs.rs/miette/latest/miette/macro.diagnostic.html
 [`std::error::Error`]: https://doc.rust-lang.org/nightly/std/error/trait.Error.html
 [`Diagnostic`]: https://docs.rs/miette/latest/miette/trait.Diagnostic.html
 [`IntoDiagnostic`]: https://docs.rs/miette/latest/miette/trait.IntoDiagnostic.html
 [`MietteHandlerOpts`]: https://docs.rs/miette/latest/miette/struct.MietteHandlerOpts.html
 [`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
+[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
 [`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
 [`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
 [`Result`]: https://docs.rs/miette/latest/miette/type.Result.html
diff --git a/README.tpl b/README.tpl
index a791344..d598eb8 100644
--- a/README.tpl
+++ b/README.tpl
@@ -4,11 +4,13 @@
 {{readme}}
 
 [`miette!`]: https://docs.rs/miette/latest/miette/macro.miette.html
+[`diagnostic!`]: https://docs.rs/miette/latest/miette/macro.diagnostic.html
 [`std::error::Error`]: https://doc.rust-lang.org/nightly/std/error/trait.Error.html
-[`Diagnostic`]: https://docs.rs/miette/latest/miette/struct.Diagnostic.html
+[`Diagnostic`]: https://docs.rs/miette/latest/miette/trait.Diagnostic.html
 [`IntoDiagnostic`]: https://docs.rs/miette/latest/miette/trait.IntoDiagnostic.html
 [`MietteHandlerOpts`]: https://docs.rs/miette/latest/miette/struct.MietteHandlerOpts.html
 [`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
+[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
 [`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
 [`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
 [`Result`]: https://docs.rs/miette/latest/miette/type.Result.html
diff --git a/src/eyreish/macros.rs b/src/eyreish/macros.rs
index 0bc5520..50605e7 100644
--- a/src/eyreish/macros.rs
+++ b/src/eyreish/macros.rs
@@ -16,7 +16,14 @@
 /// #     let resource = 0;
 /// #
 /// if !has_permission(user, resource) {
-///     bail!("permission denied for accessing {}", resource);
+#[cfg_attr(
+    not(feature = "no-format-args-capture"),
+    doc = r#"     bail!("permission denied for accessing {resource}");"#
+)]
+#[cfg_attr(
+    feature = "no-format-args-capture",
+    doc = r#"     bail!("permission denied for accessing {}", resource);"#
+)]
 /// }
 /// #     Ok(())
 /// # }
@@ -48,17 +55,37 @@
 /// #     Ok(())
 /// # }
 /// ```
+///
+/// ```
+/// use miette::{bail, Result, Severity};
+///
+/// fn divide(x: f64, y: f64) -> Result<f64> {
+///     if y.abs() < 1e-3 {
+///         bail!(
+///             severity = Severity::Warning,
+#[cfg_attr(
+    not(feature = "no-format-args-capture"),
+    doc = r#"             "dividing by value ({y}) close to 0""#
+)]
+#[cfg_attr(
+    feature = "no-format-args-capture",
+    doc = r#"             "dividing by value ({}) close to 0", y"#
+)]
+///         );
+///     }
+///     Ok(x / y)
+/// }
+/// ```
 #[macro_export]
 macro_rules! bail {
-    ($msg:literal $(,)?) => {
-        return $crate::private::Err($crate::miette!($msg));
+    ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
+        return $crate::private::Err(
+            $crate::miette!($($key = $value,)* $fmt $($arg)*)
+        );
     };
     ($err:expr $(,)?) => {
         return $crate::private::Err($crate::miette!($err));
     };
-    ($fmt:expr, $($arg:tt)*) => {
-        return $crate::private::Err($crate::miette!($fmt, $($arg)*));
-    };
 }
 
 /// Return early with an error if a condition is not satisfied.
@@ -105,11 +132,33 @@ macro_rules! bail {
 /// #     Ok(())
 /// # }
 /// ```
+///
+/// ```
+/// use miette::{ensure, Result, Severity};
+///
+/// fn divide(x: f64, y: f64) -> Result<f64> {
+///     ensure!(
+///         y.abs() >= 1e-3,
+///         severity = Severity::Warning,
+#[cfg_attr(
+    not(feature = "no-format-args-capture"),
+    doc = r#"             "dividing by value ({y}) close to 0""#
+)]
+#[cfg_attr(
+    feature = "no-format-args-capture",
+    doc = r#"             "dividing by value ({}) close to 0", y"#
+)]
+///     );
+///     Ok(x / y)
+/// }
+/// ```
 #[macro_export]
 macro_rules! ensure {
-    ($cond:expr, $msg:literal $(,)?) => {
+    ($cond:expr, $($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
         if !$cond {
-            return $crate::private::Err($crate::miette!($msg));
+            return $crate::private::Err(
+                $crate::miette!($($key = $value,)* $fmt $($arg)*)
+            );
         }
     };
     ($cond:expr, $err:expr $(,)?) => {
@@ -117,34 +166,57 @@ macro_rules! ensure {
             return $crate::private::Err($crate::miette!($err));
         }
     };
-    ($cond:expr, $fmt:expr, $($arg:tt)*) => {
-        if !$cond {
-            return $crate::private::Err($crate::miette!($fmt, $($arg)*));
-        }
-    };
 }
 
-/// Construct an ad-hoc error from a string.
+/// Construct an ad-hoc [`Report`].
 ///
-/// This evaluates to an `Error`. It can take either just a string, or a format
-/// string with arguments. It also can take any custom type which implements
-/// `Debug` and `Display`.
+/// # Examples
 ///
-/// # Example
+/// With string literal and interpolation:
+/// ```
+/// # use miette::miette;
+/// let x = 1;
+/// let y = 2;
+#[cfg_attr(
+    not(feature = "no-format-args-capture"),
+    doc = r#"let report = miette!("{x} + {} = {z}", y, z = x + y);"#
+)]
+#[cfg_attr(
+    feature = "no-format-args-capture",
+    doc = r#"let report = miette!("{} + {} = {z}", x, y, z = x + y);"#
+)]
 ///
+/// assert_eq!(report.to_string().as_str(), "1 + 2 = 3");
+///
+/// let z = x + y;
+#[cfg_attr(
+    not(feature = "no-format-args-capture"),
+    doc = r#"let report = miette!("{x} + {y} = {z}");"#
+)]
+#[cfg_attr(
+    feature = "no-format-args-capture",
+    doc = r#"let report = miette!("{} + {} = {}", x, y, z);"#
+)]
+/// assert_eq!(report.to_string().as_str(), "1 + 2 = 3");
 /// ```
-/// # type V = ();
-/// #
-/// use miette::{miette, Result};
 ///
-/// fn lookup(key: &str) -> Result<V> {
-///     if key.len() != 16 {
-///         return Err(miette!("key length must be 16 characters, got {:?}", key));
-///     }
+/// With [`diagnostic!`]-like arguments:
+/// ```
+/// use miette::{miette, LabeledSpan, Severity};
 ///
-///     // ...
-///     # Ok(())
-/// }
+/// let source = "(2 + 2".to_string();
+/// let report = miette!(
+///     // Those fields are optional
+///     severity = Severity::Error,
+///     code = "expected::rparen",
+///     help = "always close your parens",
+///     labels = vec![LabeledSpan::at_offset(6, "here")],
+///     url = "https://example.com",
+///     // Rest of the arguments are passed to `format!`
+///     // to form diagnostic message
+///     "expected closing ')'"
+/// )
+/// .with_source_code(source);
 /// ```
 ///
 /// ## `anyhow`/`eyre` Users
@@ -152,17 +224,69 @@ macro_rules! ensure {
 /// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`.
 #[macro_export]
 macro_rules! miette {
-    ($msg:literal $(,)?) => {
-        // Handle $:literal as a special case to make cargo-expanded code more
-        // concise in the common case.
-        $crate::private::new_adhoc($msg)
+    ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
+        $crate::Report::from(
+            $crate::diagnostic!($($key = $value,)* $fmt $($arg)*)
+        )
     };
     ($err:expr $(,)?) => ({
         use $crate::private::kind::*;
         let error = $err;
         (&error).miette_kind().new(error)
     });
-    ($fmt:expr, $($arg:tt)*) => {
-        $crate::private::new_adhoc(format!($fmt, $($arg)*))
-    };
+}
+
+/// Construct a [`MietteDiagnostic`] in more user-friendly way.
+///
+/// # Examples
+/// ```
+/// use miette::{diagnostic, LabeledSpan, Severity};
+///
+/// let source = "(2 + 2".to_string();
+/// let diag = diagnostic!(
+///     // Those fields are optional
+///     severity = Severity::Error,
+///     code = "expected::rparen",
+///     help = "always close your parens",
+///     labels = vec![LabeledSpan::at_offset(6, "here")],
+///     url = "https://example.com",
+///     // Rest of the arguments are passed to `format!`
+///     // to form diagnostic message
+///     "expected closing ')'",
+/// );
+/// ```
+/// Diagnostic without any fields:
+/// ```
+/// # use miette::diagnostic;
+/// let x = 1;
+/// let y = 2;
+///
+#[cfg_attr(
+    not(feature = "no-format-args-capture"),
+    doc = r#" let diag = diagnostic!("{x} + {} = {z}", y, z = x + y);"#
+)]
+#[cfg_attr(
+    feature = "no-format-args-capture",
+    doc = r#" let diag = diagnostic!("{} + {} = {z}", x, y, z = x + y);"#
+)]
+/// assert_eq!(diag.message, "1 + 2 = 3");
+///
+/// let z = x + y;
+#[cfg_attr(
+    not(feature = "no-format-args-capture"),
+    doc = r#"let diag = diagnostic!("{x} + {y} = {z}");"#
+)]
+#[cfg_attr(
+    feature = "no-format-args-capture",
+    doc = r#"let diag = diagnostic!("{} + {} = {}", x, y, z);"#
+)]
+/// assert_eq!(diag.message, "1 + 2 = 3");
+/// ```
+#[macro_export]
+macro_rules! diagnostic {
+    ($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {{
+        let mut diag = $crate::MietteDiagnostic::new(format!($fmt $($arg)*));
+        $(diag.$key = Some($value.into());)*
+        diag
+    }};
 }
diff --git a/src/lib.rs b/src/lib.rs
index b18239a..cc113ce 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -46,6 +46,7 @@
 //!   - [... multiple related errors](#-multiple-related-errors)
 //!   - [... delayed source code](#-delayed-source-code)
 //!   - [... handler options](#-handler-options)
+//!   - [... dynamic diagnostics](#-dynamic-diagnostics)
 //! - [Acknowledgements](#acknowledgements)
 //! - [License](#license)
 //!
@@ -249,7 +250,7 @@
 //! To construct your own simple adhoc error use the [miette!] macro:
 //! ```rust
 //! // my_app/lib/my_internal_file.rs
-//! use miette::{IntoDiagnostic, Result, WrapErr, miette};
+//! use miette::{miette, IntoDiagnostic, Result, WrapErr};
 //! use semver::Version;
 //!
 //! pub fn some_tool() -> Result<Version> {
@@ -590,6 +591,28 @@
 //! See the docs for [`MietteHandlerOpts`] for more details on what you can
 //! customize!
 //!
+//! ### ... dynamic diagnostics
+//!
+//! If you...
+//! - ...don't know all the possible errors upfront
+//! - ...need to serialize/deserialize errors
+//! then you may want to use [`miette!`], [`diagnostic!`] macros or
+//! [`MietteDiagnostic`] directly to create diagnostic on the fly.
+//!
+//! ```rust,ignore
+//! # use miette::{miette, LabeledSpan, Report};
+//!
+//! let source = "2 + 2 * 2 = 8".to_string();
+//! let report = miette!(
+//!   labels = vec[
+//!       LabeledSpan::at(12..13, "this should be 6"),
+//!   ],
+//!   help = "'*' has greater precedence than '+'",
+//!   "Wrong answer"
+//! ).with_source_code(source);
+//! println!("{:?}", report)
+//! ```
+//!
 //! ## Acknowledgements
 //!
 //! `miette` was not developed in a void. It owes enormous credit to various
@@ -624,6 +647,7 @@ pub use eyreish::*;
 #[cfg(feature = "fancy-no-backtrace")]
 pub use handler::*;
 pub use handlers::*;
+pub use miette_diagnostic::*;
 pub use named_source::*;
 #[cfg(feature = "fancy")]
 pub use panic::*;
@@ -638,6 +662,7 @@ mod handler;
 mod handlers;
 #[doc(hidden)]
 pub mod macro_helpers;
+mod miette_diagnostic;
 mod named_source;
 #[cfg(feature = "fancy")]
 mod panic;
diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs
new file mode 100644
index 0000000..29a0c80
--- /dev/null
+++ b/src/miette_diagnostic.rs
@@ -0,0 +1,250 @@
+use std::{
+    error::Error,
+    fmt::{Debug, Display},
+};
+
+use crate::{Diagnostic, LabeledSpan, Severity};
+
+/// Diagnostic that can be created at runtime.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct MietteDiagnostic {
+    /// Displayed diagnostic message
+    pub message: String,
+    /// Unique diagnostic code to look up more information
+    /// about this Diagnostic. Ideally also globally unique, and documented
+    /// in the toplevel crate's documentation for easy searching.
+    /// Rust path format (`foo::bar::baz`) is recommended, but more classic
+    /// codes like `E0123` will work just fine
+    pub code: Option<String>,
+    /// [`Diagnostic`] severity. Intended to be used by
+    /// [`ReportHandler`](crate::ReportHandler)s to change the way different
+    /// [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`]
+    pub severity: Option<Severity>,
+    /// Additional help text related to this Diagnostic
+    pub help: Option<String>,
+    /// URL to visit for a more detailed explanation/help about this
+    /// [`Diagnostic`].
+    pub url: Option<String>,
+    /// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`]
+    pub labels: Option<Vec<LabeledSpan>>,
+}
+
+impl Display for MietteDiagnostic {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", &self.message)
+    }
+}
+
+impl Error for MietteDiagnostic {}
+
+impl Diagnostic for MietteDiagnostic {
+    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.code
+            .as_ref()
+            .map(Box::new)
+            .map(|c| c as Box<dyn Display>)
+    }
+
+    fn severity(&self) -> Option<Severity> {
+        self.severity
+    }
+
+    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.help
+            .as_ref()
+            .map(Box::new)
+            .map(|c| c as Box<dyn Display>)
+    }
+
+    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
+        self.url
+            .as_ref()
+            .map(Box::new)
+            .map(|c| c as Box<dyn Display>)
+    }
+
+    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
+        self.labels
+            .as_ref()
+            .map(|ls| ls.iter().cloned())
+            .map(Box::new)
+            .map(|b| b as Box<dyn Iterator<Item = LabeledSpan>>)
+    }
+}
+
+impl MietteDiagnostic {
+    /// Create a new dynamic diagnostic with the given message.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, MietteDiagnostic, Severity};
+    ///
+    /// let diag = MietteDiagnostic::new("Oops, something went wrong!");
+    /// assert_eq!(diag.to_string(), "Oops, something went wrong!");
+    /// assert_eq!(diag.message, "Oops, something went wrong!");
+    /// ```
+    pub fn new(message: impl Into<String>) -> Self {
+        Self {
+            message: message.into(),
+            labels: None,
+            severity: None,
+            code: None,
+            help: None,
+            url: None,
+        }
+    }
+
+    /// Return new diagnostic with the given code.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, MietteDiagnostic};
+    ///
+    /// let diag = MietteDiagnostic::new("Oops, something went wrong!").with_code("foo::bar::baz");
+    /// assert_eq!(diag.message, "Oops, something went wrong!");
+    /// assert_eq!(diag.code, Some("foo::bar::baz".to_string()));
+    /// ```
+    pub fn with_code(mut self, code: impl Into<String>) -> Self {
+        self.code = Some(code.into());
+        self
+    }
+
+    /// Return new diagnostic with the given severity.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, MietteDiagnostic, Severity};
+    ///
+    /// let diag = MietteDiagnostic::new("I warn you to stop!").with_severity(Severity::Warning);
+    /// assert_eq!(diag.message, "I warn you to stop!");
+    /// assert_eq!(diag.severity, Some(Severity::Warning));
+    /// ```
+    pub fn with_severity(mut self, severity: Severity) -> Self {
+        self.severity = Some(severity);
+        self
+    }
+
+    /// Return new diagnostic with the given help message.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, MietteDiagnostic};
+    ///
+    /// let diag = MietteDiagnostic::new("PC is not working").with_help("Try to reboot it again");
+    /// assert_eq!(diag.message, "PC is not working");
+    /// assert_eq!(diag.help, Some("Try to reboot it again".to_string()));
+    /// ```
+    pub fn with_help(mut self, help: impl Into<String>) -> Self {
+        self.help = Some(help.into());
+        self
+    }
+
+    /// Return new diagnostic with the given URL.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, MietteDiagnostic};
+    ///
+    /// let diag = MietteDiagnostic::new("PC is not working")
+    ///     .with_url("https://letmegooglethat.com/?q=Why+my+pc+doesn%27t+work");
+    /// assert_eq!(diag.message, "PC is not working");
+    /// assert_eq!(
+    ///     diag.url,
+    ///     Some("https://letmegooglethat.com/?q=Why+my+pc+doesn%27t+work".to_string())
+    /// );
+    /// ```
+    pub fn with_url(mut self, url: impl Into<String>) -> Self {
+        self.url = Some(url.into());
+        self
+    }
+
+    /// Return new diagnostic with the given label.
+    ///
+    /// Discards previous labels
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
+    ///
+    /// let source = "cpp is the best language";
+    ///
+    /// let label = LabeledSpan::at(0..3, "This should be Rust");
+    /// let diag = MietteDiagnostic::new("Wrong best language").with_label(label.clone());
+    /// assert_eq!(diag.message, "Wrong best language");
+    /// assert_eq!(diag.labels, Some(vec![label]));
+    /// ```
+    pub fn with_label(mut self, label: impl Into<LabeledSpan>) -> Self {
+        self.labels = Some(vec![label.into()]);
+        self
+    }
+
+    /// Return new diagnostic with the given labels.
+    ///
+    /// Discards previous labels
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
+    ///
+    /// let source = "helo wrld";
+    ///
+    /// let labels = vec![
+    ///     LabeledSpan::at_offset(3, "add 'l'"),
+    ///     LabeledSpan::at_offset(6, "add 'r'"),
+    /// ];
+    /// let diag = MietteDiagnostic::new("Typos in 'hello world'").with_labels(labels.clone());
+    /// assert_eq!(diag.message, "Typos in 'hello world'");
+    /// assert_eq!(diag.labels, Some(labels));
+    /// ```
+    pub fn with_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
+        self.labels = Some(labels.into_iter().collect());
+        self
+    }
+
+    /// Return new diagnostic with new label added to the existing ones.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
+    ///
+    /// let source = "helo wrld";
+    ///
+    /// let label1 = LabeledSpan::at_offset(3, "add 'l'");
+    /// let label2 = LabeledSpan::at_offset(6, "add 'r'");
+    /// let diag = MietteDiagnostic::new("Typos in 'hello world'")
+    ///     .and_label(label1.clone())
+    ///     .and_label(label2.clone());
+    /// assert_eq!(diag.message, "Typos in 'hello world'");
+    /// assert_eq!(diag.labels, Some(vec![label1, label2]));
+    /// ```
+    pub fn and_label(mut self, label: impl Into<LabeledSpan>) -> Self {
+        let mut labels = self.labels.unwrap_or_default();
+        labels.push(label.into());
+        self.labels = Some(labels);
+        self
+    }
+
+    /// Return new diagnostic with new labels added to the existing ones.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
+    ///
+    /// let source = "helo wrld";
+    ///
+    /// let label1 = LabeledSpan::at_offset(3, "add 'l'");
+    /// let label2 = LabeledSpan::at_offset(6, "add 'r'");
+    /// let label3 = LabeledSpan::at_offset(9, "add '!'");
+    /// let diag = MietteDiagnostic::new("Typos in 'hello world!'")
+    ///     .and_label(label1.clone())
+    ///     .and_labels([label2.clone(), label3.clone()]);
+    /// assert_eq!(diag.message, "Typos in 'hello world!'");
+    /// assert_eq!(diag.labels, Some(vec![label1, label2, label3]));
+    /// ```
+    pub fn and_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
+        let mut all_labels = self.labels.unwrap_or_default();
+        all_labels.extend(labels.into_iter());
+        self.labels = Some(all_labels);
+        self
+    }
+}
diff --git a/src/protocol.rs b/src/protocol.rs
index 5331249..0f606f6 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -160,7 +160,7 @@ impl From<Box<dyn std::error::Error + Send + Sync>> for Box<dyn Diagnostic + Sen
 /**
 [`Diagnostic`] severity. Intended to be used by
 [`ReportHandler`](crate::ReportHandler)s to change the way different
-[`Diagnostic`]s are displayed.
+[`Diagnostic`]s are displayed. Defaults to [`Severity::Error`].
 */
 #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
 pub enum Severity {
@@ -169,9 +169,16 @@ pub enum Severity {
     /// Warning. Please take note.
     Warning,
     /// Critical failure. The program cannot continue.
+    /// This is the default severity, if you don't specify another one.
     Error,
 }
 
+impl Default for Severity {
+    fn default() -> Self {
+        Severity::Error
+    }
+}
+
 /**
 Represents readable source code of some sort.
 
@@ -218,6 +225,54 @@ impl LabeledSpan {
         }
     }
 
+    /// Makes a new label at specified span
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::LabeledSpan;
+    ///
+    /// let source = "Cpp is the best";
+    /// let label = LabeledSpan::at(0..3, "should be Rust");
+    /// assert_eq!(
+    ///     label,
+    ///     LabeledSpan::new(Some("should be Rust".to_string()), 0, 3)
+    /// )
+    /// ```
+    pub fn at(span: impl Into<SourceSpan>, label: impl Into<String>) -> Self {
+        Self::new_with_span(Some(label.into()), span)
+    }
+
+    /// Makes a new label that points at a specific offset.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::LabeledSpan;
+    ///
+    /// let source = "(2 + 2";
+    /// let label = LabeledSpan::at_offset(4, "expected a closing parenthesis");
+    /// assert_eq!(
+    ///     label,
+    ///     LabeledSpan::new(Some("expected a closing parenthesis".to_string()), 4, 0)
+    /// )
+    /// ```
+    pub fn at_offset(offset: ByteOffset, label: impl Into<String>) -> Self {
+        Self::new(Some(label.into()), offset, 0)
+    }
+
+    /// Makes a new label without text, that underlines a specific span.
+    ///
+    /// # Examples
+    /// ```
+    /// use miette::LabeledSpan;
+    ///
+    /// let source = "You have an eror here";
+    /// let label = LabeledSpan::underline(12..16);
+    /// assert_eq!(label, LabeledSpan::new(None, 12, 4))
+    /// ```
+    pub fn underline(span: impl Into<SourceSpan>) -> Self {
+        Self::new_with_span(None, span)
+    }
+
     /// Gets the (optional) label string for this `LabeledSpan`.
     pub fn label(&self) -> Option<&str> {
         self.label.as_deref()


Test patch:
diff --git a/tests/test_chain.rs b/tests/test_chain.rs
index 3fdd4cd..29d3927 100644
--- a/tests/test_chain.rs
+++ b/tests/test_chain.rs
@@ -1,7 +1,7 @@
 use miette::{miette, Report};
 
 fn error() -> Report {
-    miette!(0).wrap_err(1).wrap_err(2).wrap_err(3)
+    miette!("0").wrap_err(1).wrap_err(2).wrap_err(3)
 }
 
 #[test]
diff --git a/tests/test_downcast.rs b/tests/test_downcast.rs
index 2823177..cf5ab2e 100644
--- a/tests/test_downcast.rs
+++ b/tests/test_downcast.rs
@@ -3,7 +3,7 @@ mod drop;
 
 use self::common::*;
 use self::drop::{DetectDrop, Flag};
-use miette::{Diagnostic, Report};
+use miette::{Diagnostic, MietteDiagnostic, Report};
 use std::error::Error as StdError;
 use std::fmt::{self, Display};
 use std::io;
@@ -12,11 +12,19 @@ use std::io;
 fn test_downcast() {
     assert_eq!(
         "oh no!",
-        bail_literal().unwrap_err().downcast::<&str>().unwrap(),
+        bail_literal()
+            .unwrap_err()
+            .downcast::<MietteDiagnostic>()
+            .unwrap()
+            .message,
     );
     assert_eq!(
         "oh no!",
-        bail_fmt().unwrap_err().downcast::<String>().unwrap(),
+        bail_fmt()
+            .unwrap_err()
+            .downcast::<MietteDiagnostic>()
+            .unwrap()
+            .message,
     );
     assert_eq!(
         "oh no!",
@@ -32,11 +40,19 @@ fn test_downcast() {
 fn test_downcast_ref() {
     assert_eq!(
         "oh no!",
-        *bail_literal().unwrap_err().downcast_ref::<&str>().unwrap(),
+        bail_literal()
+            .unwrap_err()
+            .downcast_ref::<MietteDiagnostic>()
+            .unwrap()
+            .message,
     );
     assert_eq!(
         "oh no!",
-        bail_fmt().unwrap_err().downcast_ref::<String>().unwrap(),
+        bail_fmt()
+            .unwrap_err()
+            .downcast_ref::<MietteDiagnostic>()
+            .unwrap()
+            .message,
     );
     assert_eq!(
         "oh no!",
@@ -52,11 +68,19 @@ fn test_downcast_ref() {
 fn test_downcast_mut() {
     assert_eq!(
         "oh no!",
-        *bail_literal().unwrap_err().downcast_mut::<&str>().unwrap(),
+        bail_literal()
+            .unwrap_err()
+            .downcast_mut::<MietteDiagnostic>()
+            .unwrap()
+            .message,
     );
     assert_eq!(
         "oh no!",
-        bail_fmt().unwrap_err().downcast_mut::<String>().unwrap(),
+        bail_fmt()
+            .unwrap_err()
+            .downcast_mut::<MietteDiagnostic>()
+            .unwrap()
+            .message,
     );
     assert_eq!(
         "oh no!",