diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a527787 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch"] diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..fd15b8f --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,65 @@ +name: CI +on: [pull_request, push] +jobs: + rustfmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + clippy: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: clippy + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-targets --no-default-features -- -D clippy::all + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: --all-features --all-targets -- -D clippy::all + test: + name: Test + needs: [clippy, rustfmt] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macOS-latest, ubuntu-latest, windows-latest] + toolchain: + - 1.56.0 # Minimum. + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.toolchain }} + override: true + - uses: actions-rs/cargo@v1 + with: + command: test + args: --no-default-features --verbose + - uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70733e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +target/ +**/*.rs.bk +*.iml +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a77b41a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "tardar" +version = "0.0.0" +rust-version = "1.56.0" +edition = "2021" +authors = ["Sean Olson "] +description = "Extensions for diagnostic error handling with `miette`." +repository = "https://github.com/olson-sean-k/tardar" +readme = "README.md" +license = "MIT" +keywords = ["diagnostics"] + +[dependencies] + +[dependencies.miette] +version = "^5.1.0" +default-features = false + +[dependencies.vec1] +version = "^1.8.0" +default-features = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..a626a49 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +**Tardar** is a Rust library that provides extensions for the [`miette`] crate. +Diagnostic `Result`s are the primary extension, which pair an output with +aggregated `Diagnostic`s in both the `Ok` and `Err` variants. + +[![GitHub](https://img.shields.io/badge/GitHub-olson--sean--k/tardar-8da0cb?logo=github&style=for-the-badge)](https://github.com/olson-sean-k/tardar) +[![docs.rs](https://img.shields.io/badge/docs.rs-tardar-66c2a5?logo=rust&style=for-the-badge)](https://docs.rs/tardar) +[![crates.io](https://img.shields.io/crates/v/tardar.svg?logo=rust&style=for-the-badge)](https://crates.io/crates/tardar) + +[`miette`]: https://crates.io/crates/miette diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..5cee99c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +control_brace_style = "ClosingNextLine" +format_code_in_doc_comments = true diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..69dc8a3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,189 @@ +//! **Tardar** is a library that provides extensions for the [`miette`] crate. [Diagnostic +//! `Result`][`DiagnosticResult`]s are the primary extension, which pair an output with aggregated +//! [`Diagnostic`]s in both the `Ok` and `Err` variants. +//! +//! [`Diagnostic`]: miette::Diagnostic +//! [`DiagnosticResult`]: crate::DiagnosticResult +//! [`Result`]: std::result::Result +//! +//! [`miette`]: https://crates.io/crates/miette + +use miette::Diagnostic; +use vec1::{vec1, Vec1}; + +pub mod integration { + pub mod miette { + #[doc(hidden)] + pub use ::miette::*; + } +} + +pub mod prelude { + pub use crate::{DiagnosticResultExt as _, IteratorExt as _, ResultExt as _}; +} + +pub type BoxedDiagnostic<'d> = Box; + +/// `Result` that includes [`Diagnostic`]s on both success and failure. +/// +/// On success, the `Ok` variant contains zero or more diagnostics and a non-diagnostic output `T`. +/// On failure, the `Err` variant contains one or more diagnostics, where at least one of the +/// diagnostics is an error. +/// +/// See [`DiagnosticResultExt`]. +/// +/// [`Diagnostic`]: miette::Diagnostic +/// [`DiagnosticResultExt`]: crate::DiagnosticResultExt +pub type DiagnosticResult<'d, T> = Result<(T, Vec>), Vec1>>; + +/// Extension methods for [`Iterator`]s. +/// +/// [`Iterator`]: std::iter::Iterator +pub trait IteratorExt: Iterator + Sized { + /// Converts from a type that implements `Iterator>` into + /// `DiagnosticResult<'d, ()>`. + /// + /// The [`Diagnostic`] items of the iterator are interpreted as non-errors. Note that the + /// [`Severity`] is not examined and so the [`Diagnostic`]s may have error-level severities + /// despite being interpreted as non-errors. + /// + /// [`Diagnostic`]: miette::Diagnostic + /// [`Severity`]: miette::Severity + fn into_non_error_diagnostic<'d>(self) -> DiagnosticResult<'d, ()> + where + Self: Iterator>; +} + +impl IteratorExt for I +where + I: Iterator, +{ + fn into_non_error_diagnostic<'d>(self) -> DiagnosticResult<'d, ()> + where + Self: Iterator>, + { + Ok(((), self.collect())) + } +} + +/// Extension methods for [`Result`]s. +/// +/// [`Result`]: std::result::Result +pub trait ResultExt { + /// Converts from `Result` into `DiagnosticResult<'d, T>`. + /// + /// The error type `E` must be a [`Diagnostic`] and is interpreted as an error. Note that the + /// [`Severity`] is not examined and so the [`Diagnostic`] may have a non-error severity + /// despite being interpreted as an error. + /// + /// [`Diagnostic`]: miette::Diagnostic + /// [`Severity`]: miette::Severity + fn into_error_diagnostic<'d>(self) -> DiagnosticResult<'d, T> + where + E: 'd + Diagnostic; +} + +impl ResultExt for Result { + fn into_error_diagnostic<'d>(self) -> DiagnosticResult<'d, T> + where + E: 'd + Diagnostic, + { + match self { + Ok(output) => Ok((output, vec![])), + Err(error) => Err(vec1![Box::new(error) as Box]), + } + } +} + +/// Extension methods for [`DiagnosticResult`]s. +/// +/// [`DiagnosticResult`]: crate::DiagnosticResult +pub trait DiagnosticResultExt<'d, T> { + /// Converts from `DiagnosticResult<'_, T>` into `Option`. + /// + /// This function is similar to [`Result::ok`], but gets only the non-diagnostic output `T` + /// from the `Ok` variant in [`DiagnosticResult`], discarding diagnostics. + /// + /// [`DiagnosticResult`]: crate::DiagnosticResult + /// [`Result::ok`]: std::result::Result::ok + fn ok_output(self) -> Option; + + /// Gets the [`Diagnostic`]s associated with the [`DiagnosticResult`]. + /// + /// Both the success and failure case may include diagnostics. + /// + /// [`Diagnostic`]: miette::Diagnostic + /// [`DiagnosticResult`]: crate::DiagnosticResult + fn diagnostics(&self) -> &[BoxedDiagnostic<'d>]; + + /// Maps `DiagnosticResult<'d, T>` into `DiagnosticResult<'d, U>` by applying a function over + /// the non-diagnostic output of the `Ok` variant. + /// + /// This function is similar to [`Result::map`], but maps only the non-diagnostic output `T` + /// from the `Ok` variant in [`DiagnosticResult`], ignoring diagnostics. + /// + /// [`DiagnosticResult`]: crate::DiagnosticResult + /// [`Result::map`]: std::result::Result::map + fn map_output(self, f: F) -> DiagnosticResult<'d, U> + where + F: FnOnce(T) -> U; + + /// Calls the given function if the `DiagnosticResult` is `Ok` and otherwise returns the `Err` + /// variant of the `DiagnosticResult`. + /// + /// This function is similar to [`Result::and_then`], but additionally forwards and collects + /// diagnostics. + /// + /// [`DiagnosticResult`]: crate::DiagnosticResult + /// [`Result::and_then`]: std::result::Result::and_then + fn and_then_diagnose(self, f: F) -> DiagnosticResult<'d, U> + where + F: FnOnce(T) -> DiagnosticResult<'d, U>; +} + +impl<'d, T> DiagnosticResultExt<'d, T> for DiagnosticResult<'d, T> { + fn ok_output(self) -> Option { + match self { + Ok((output, _)) => Some(output), + _ => None, + } + } + + fn diagnostics(&self) -> &[BoxedDiagnostic<'d>] { + match self { + Ok((_, ref diagnostics)) => diagnostics, + Err(ref diagnostics) => diagnostics, + } + } + + fn map_output(self, f: F) -> DiagnosticResult<'d, U> + where + F: FnOnce(T) -> U, + { + match self { + Ok((output, diagnostics)) => Ok((f(output), diagnostics)), + Err(diagnostics) => Err(diagnostics), + } + } + + fn and_then_diagnose(self, f: F) -> DiagnosticResult<'d, U> + where + F: FnOnce(T) -> DiagnosticResult<'d, U>, + { + match self { + Ok((output, mut diagnostics)) => match f(output) { + Ok((output, tail)) => { + diagnostics.extend(tail); + Ok((output, diagnostics)) + } + Err(tail) => { + diagnostics.extend(tail); + Err(diagnostics + .try_into() + .expect("diagnostic failure with no errors")) + } + }, + Err(diagnostics) => Err(diagnostics), + } + } +}