diff --git a/AGENTS.md b/AGENTS.md index bea39459..6eca62dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,7 +155,9 @@ inward (core/macros) when changing fundamentals, outward (main) only to adjust t - **`crates/standalone/error/`** — pluggable error backends implementing the `cgp-error` traits: `cgp-error-anyhow`, `cgp-error-eyre`, `cgp-error-std`. These are opt-in and not part of the - default `cgp` facade. + default `cgp` facade. The knowledge base documents them as a project of their own, under + [`projects/error/`](https://github.com/contextgeneric/cgp-knowledge-base/tree/main/projects/error), + and their tests are the `error_backends` target of `cgp-tests`. - **`crates/tests/`** — `cgp-tests` exercises real wiring and the user-facing macros end-to-end; `cgp-macro-tests` covers parser corner cases plus **expansion snapshots** via the `snapshot_*` diff --git a/Cargo.lock b/Cargo.lock index 74f19690..91afa091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,15 @@ version = 4 [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" @@ -275,6 +281,9 @@ name = "cgp-tests" version = "0.8.0-alpha" dependencies = [ "cgp", + "cgp-error-anyhow", + "cgp-error-eyre", + "cgp-error-std", "cgp-macro-core", "cgp-macro-test-util", "futures", @@ -328,10 +337,11 @@ dependencies = [ [[package]] name = "eyre" -version = "0.6.12" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +checksum = "c08309dbcc659c5549a24ddb9b27027640641b282ef5768267c7e675558986a3" dependencies = [ + "autocfg", "indenter", "once_cell", ] diff --git a/Cargo.toml b/Cargo.toml index 828076b2..0d9a92d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,10 @@ cgp-dispatch = { version = "0.8.0-alpha", path = "./crates/extra/ cgp-run = { version = "0.8.0-alpha", path = "./crates/extra/cgp-run" } cgp-runtime = { version = "0.8.0-alpha", path = "./crates/extra/cgp-runtime" } +cgp-error-anyhow = { version = "0.8.0-alpha", path = "./crates/standalone/error/cgp-error-anyhow" } +cgp-error-eyre = { version = "0.8.0-alpha", path = "./crates/standalone/error/cgp-error-eyre" } +cgp-error-std = { version = "0.8.0-alpha", path = "./crates/standalone/error/cgp-error-std" } + cgp-async-macro = { version = "0.8.0-alpha", path = "./crates/macros/cgp-async-macro" } cgp-macro = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro" } cgp-macro-core = { version = "0.8.0-alpha", path = "./crates/macros/cgp-macro-core" } diff --git a/crates/standalone/error/cgp-error-anyhow/Cargo.toml b/crates/standalone/error/cgp-error-anyhow/Cargo.toml index 59db0d20..82b88f62 100644 --- a/crates/standalone/error/cgp-error-anyhow/Cargo.toml +++ b/crates/standalone/error/cgp-error-anyhow/Cargo.toml @@ -1,16 +1,17 @@ [package] name = "cgp-error-anyhow" version = "0.8.0-alpha" +readme = "./README.md" edition = { workspace = true } license = { workspace = true } repository = { workspace = true } authors = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } keywords = { workspace = true } -description = """ - Context-generic programming error handlers implemented using anyhow -""" +categories = { workspace = true } +description = "CGP error providers that make anyhow::Error a context's abstract error type." [dependencies] cgp = { version = "0.8.0-alpha", path = "../../../main/cgp-core", package = "cgp-core" } -anyhow = { version = "1.0.95", default-features = false } +anyhow = { version = "1.0.104", default-features = false } diff --git a/crates/standalone/error/cgp-error-anyhow/README.md b/crates/standalone/error/cgp-error-anyhow/README.md new file mode 100644 index 00000000..a638ab15 --- /dev/null +++ b/crates/standalone/error/cgp-error-anyhow/README.md @@ -0,0 +1,45 @@ +# cgp-error-anyhow + +`cgp-error-anyhow` makes [`anyhow::Error`](https://docs.rs/anyhow) the abstract error type of a +[CGP](https://contextgeneric.dev) context, and supplies the providers that raise errors into it and +add context to it. Code written against `HasErrorType`, `CanRaiseError`, and `CanWrapError` stays +generic; only the context's wiring names anyhow. + +| Provider | Wire it to | What it does | +|---|---|---| +| `UseAnyhowError` | `ErrorTypeProviderComponent` | sets the context's `Error` to `anyhow::Error` | +| `RaiseAnyhowError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | raises a standard error without formatting it, so `downcast_ref` still finds it; wraps a `Display + Send + Sync + 'static` detail as anyhow context | +| `DebugAnyhowError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | raises or wraps any `Debug` value as a message formatted with `{:?}` | +| `DisplayAnyhowError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | raises or wraps any `Display` value as a message formatted with `{}` | + +A context usually routes each source type to the provider that suits it: + +```rust,ignore +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_anyhow::{DisplayAnyhowError, RaiseAnyhowError, UseAnyhowError}; + +pub struct App; + +delegate_components! { + App { + open ErrorRaiserComponent; + + ErrorTypeProviderComponent: UseAnyhowError, + @ErrorRaiserComponent.std::io::Error: RaiseAnyhowError, + @ErrorRaiserComponent.String: DisplayAnyhowError, + ErrorWrapperComponent: RaiseAnyhowError, + } +} + +let error = App::raise_error(std::io::Error::other("disk full")); +let error = App::wrap_error(error, "while saving"); +assert_eq!(format!("{error:#}"), "while saving: disk full"); +``` + +A `String` needs `DisplayAnyhowError` or `DebugAnyhowError`, because it is not a standard error. +`DebugAnyhowError` prints it with quotes. The crate is `no_std` and builds anyhow without its `std` +feature. Enabling that feature elsewhere in the dependency graph turns on the parts of anyhow that +need `std`, such as backtrace capture. + +The crate re-exports `anyhow::Error` as `cgp_error_anyhow::Error`. diff --git a/crates/standalone/error/cgp-error-anyhow/src/impls/debug_error.rs b/crates/standalone/error/cgp-error-anyhow/src/impls/debug_error.rs index 79a0f045..c7f95332 100644 --- a/crates/standalone/error/cgp-error-anyhow/src/impls/debug_error.rs +++ b/crates/standalone/error/cgp-error-anyhow/src/impls/debug_error.rs @@ -1,27 +1,29 @@ use alloc::format; use core::fmt::Debug; -use anyhow::{Error, anyhow}; +use anyhow::anyhow; use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; +/// Raises any `Debug` value into [`anyhow::Error`] as a message formatted with `{:?}`, and wraps a +/// `Debug` detail the same way. The original value is not kept. pub struct DebugAnyhowError; -#[cgp_provider] -impl ErrorRaiser for DebugAnyhowError +#[cgp_impl(DebugAnyhowError)] +#[use_type(HasErrorType.{Error = anyhow::Error})] +impl ErrorRaiser where - Context: HasErrorType, E: Debug, { fn raise_error(e: E) -> Error { - anyhow!("{:?}", e) + anyhow!("{e:?}") } } -#[cgp_provider] -impl ErrorWrapper for DebugAnyhowError +#[cgp_impl(DebugAnyhowError)] +#[use_type(HasErrorType.{Error = anyhow::Error})] +impl ErrorWrapper where - Context: HasErrorType, Detail: Debug, { fn wrap_error(error: Error, detail: Detail) -> Error { diff --git a/crates/standalone/error/cgp-error-anyhow/src/impls/display_error.rs b/crates/standalone/error/cgp-error-anyhow/src/impls/display_error.rs index 0d909098..c216039d 100644 --- a/crates/standalone/error/cgp-error-anyhow/src/impls/display_error.rs +++ b/crates/standalone/error/cgp-error-anyhow/src/impls/display_error.rs @@ -1,16 +1,18 @@ use alloc::string::ToString; use core::fmt::Display; -use anyhow::{Error, anyhow}; +use anyhow::anyhow; use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; +/// Raises any `Display` value into [`anyhow::Error`] as a message formatted with `{}`, and wraps a +/// `Display` detail the same way. The original value is not kept. pub struct DisplayAnyhowError; -#[cgp_provider] -impl ErrorRaiser for DisplayAnyhowError +#[cgp_impl(DisplayAnyhowError)] +#[use_type(HasErrorType.{Error = anyhow::Error})] +impl ErrorRaiser where - Context: HasErrorType, E: Display, { fn raise_error(e: E) -> Error { @@ -18,10 +20,10 @@ where } } -#[cgp_provider] -impl ErrorWrapper for DisplayAnyhowError +#[cgp_impl(DisplayAnyhowError)] +#[use_type(HasErrorType.{Error = anyhow::Error})] +impl ErrorWrapper where - Context: HasErrorType, Detail: Display, { fn wrap_error(error: Error, detail: Detail) -> Error { diff --git a/crates/standalone/error/cgp-error-anyhow/src/impls/raise_anyhow_error.rs b/crates/standalone/error/cgp-error-anyhow/src/impls/raise_anyhow_error.rs index 4c6dc4e0..74ae5ec4 100644 --- a/crates/standalone/error/cgp-error-anyhow/src/impls/raise_anyhow_error.rs +++ b/crates/standalone/error/cgp-error-anyhow/src/impls/raise_anyhow_error.rs @@ -1,16 +1,17 @@ use core::error::Error as StdError; use core::fmt::Display; -use anyhow::Error; use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; +/// Raises a standard error into [`anyhow::Error`] without formatting it, so the source stays +/// available to `downcast_ref` and to the error chain, and wraps a detail as anyhow context. pub struct RaiseAnyhowError; #[cgp_impl(RaiseAnyhowError)] -impl ErrorRaiser for Context +#[use_type(HasErrorType.{Error = anyhow::Error})] +impl ErrorRaiser where - Context: HasErrorType, E: StdError + Send + Sync + 'static, { fn raise_error(e: E) -> Error { @@ -19,9 +20,9 @@ where } #[cgp_impl(RaiseAnyhowError)] -impl ErrorWrapper for Context +#[use_type(HasErrorType.{Error = anyhow::Error})] +impl ErrorWrapper where - Context: HasErrorType, Detail: Display + Send + Sync + 'static, { fn wrap_error(error: Error, detail: Detail) -> Error { diff --git a/crates/standalone/error/cgp-error-anyhow/src/impls/use_anyhow_error.rs b/crates/standalone/error/cgp-error-anyhow/src/impls/use_anyhow_error.rs index a21b24e5..77d456e0 100644 --- a/crates/standalone/error/cgp-error-anyhow/src/impls/use_anyhow_error.rs +++ b/crates/standalone/error/cgp-error-anyhow/src/impls/use_anyhow_error.rs @@ -1,8 +1,10 @@ -use anyhow::Error; use cgp::error::{ErrorTypeProvider, ErrorTypeProviderComponent}; use cgp::prelude::*; -#[cgp_new_provider] -impl ErrorTypeProvider for UseAnyhowError { - type Error = Error; +/// Sets the context's abstract error type to [`anyhow::Error`]. +pub struct UseAnyhowError; + +#[cgp_impl(UseAnyhowError)] +impl ErrorTypeProvider { + type Error = anyhow::Error; } diff --git a/crates/standalone/error/cgp-error-anyhow/src/lib.rs b/crates/standalone/error/cgp-error-anyhow/src/lib.rs index bd6ecc5b..1fa6362c 100644 --- a/crates/standalone/error/cgp-error-anyhow/src/lib.rs +++ b/crates/standalone/error/cgp-error-anyhow/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] #![no_std] extern crate alloc; diff --git a/crates/standalone/error/cgp-error-eyre/Cargo.toml b/crates/standalone/error/cgp-error-eyre/Cargo.toml index eda992a3..39700636 100644 --- a/crates/standalone/error/cgp-error-eyre/Cargo.toml +++ b/crates/standalone/error/cgp-error-eyre/Cargo.toml @@ -1,16 +1,23 @@ [package] name = "cgp-error-eyre" version = "0.8.0-alpha" +readme = "./README.md" edition = { workspace = true } license = { workspace = true } repository = { workspace = true } authors = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } keywords = { workspace = true } -description = """ - Context-generic programming error handlers implemented using eyre -""" +categories = [ "rust-patterns" ] +description = "CGP error providers that make eyre::Report a context's abstract error type." [dependencies] cgp = { version = "0.8.0-alpha", path = "../../../main/cgp-core", package = "cgp-core" } -eyre = { version = "0.6.12", default-features = false } +# `auto-install` installs eyre's default report handler on first use. Without it, building any +# `eyre::Report` panics unless the application has called `eyre::set_hook` first. +# +# `track-caller` is left off: every report is built inside one of this crate's providers, behind +# CGP's generated forwarding impls, so the location it records is always a line in this crate +# rather than the caller's. +eyre = { version = "0.6.14", default-features = false, features = [ "auto-install" ] } diff --git a/crates/standalone/error/cgp-error-eyre/README.md b/crates/standalone/error/cgp-error-eyre/README.md new file mode 100644 index 00000000..5cc8a2b0 --- /dev/null +++ b/crates/standalone/error/cgp-error-eyre/README.md @@ -0,0 +1,50 @@ +# cgp-error-eyre + +`cgp-error-eyre` makes [`eyre::Report`](https://docs.rs/eyre) the abstract error type of a +[CGP](https://contextgeneric.dev) context, and supplies the providers that raise errors into it and +add context to it. Code written against `HasErrorType`, `CanRaiseError`, and `CanWrapError` stays +generic; only the context's wiring names eyre. + +| Provider | Wire it to | What it does | +|---|---|---| +| `UseEyreError` | `ErrorTypeProviderComponent` | sets the context's `Error` to `eyre::Report` | +| `RaiseEyreError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | raises a standard error without formatting it, so `downcast_ref` still finds it; wraps a `Display + Send + Sync + 'static` detail with `wrap_err` | +| `DebugEyreError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | raises or wraps any `Debug` value as a message formatted with `{:?}` | +| `DisplayEyreError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | raises or wraps any `Display` value as a message formatted with `{}` | + +A context usually routes each source type to the provider that suits it: + +```rust,ignore +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_eyre::{DisplayEyreError, RaiseEyreError, UseEyreError}; + +pub struct App; + +delegate_components! { + App { + open ErrorRaiserComponent; + + ErrorTypeProviderComponent: UseEyreError, + @ErrorRaiserComponent.std::io::Error: RaiseEyreError, + @ErrorRaiserComponent.String: DisplayEyreError, + ErrorWrapperComponent: RaiseEyreError, + } +} + +let error = App::raise_error(std::io::Error::other("disk full")); +let error = App::wrap_error(error, "while saving"); +assert_eq!(format!("{error:#}"), "while saving: disk full"); +``` + +A `String` needs `DisplayEyreError` or `DebugEyreError`, because it is not a standard error. +`DebugEyreError` prints it with quotes. + +The crate enables eyre's `auto-install` feature, so eyre's default report handler is installed the +first time a report is built. To use another handler, such as `color-eyre`, install it with +`eyre::set_hook` before the first error is raised; once a report exists, `set_hook` returns an +error. The crate leaves eyre's `track-caller` feature off, because every report is built inside one +of its providers and the recorded location would name that line rather than the caller. eyre +requires `std`, so this crate does too. + +The crate re-exports `eyre::Error`, eyre's alias for `Report`, as `cgp_error_eyre::Error`. diff --git a/crates/standalone/error/cgp-error-eyre/src/impls/debug_error.rs b/crates/standalone/error/cgp-error-eyre/src/impls/debug_error.rs index 2008f452..9165c5f4 100644 --- a/crates/standalone/error/cgp-error-eyre/src/impls/debug_error.rs +++ b/crates/standalone/error/cgp-error-eyre/src/impls/debug_error.rs @@ -3,25 +3,27 @@ use core::fmt::Debug; use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; -use eyre::{Error, eyre}; +use eyre::eyre; +/// Raises any `Debug` value into [`eyre::Report`] as a message formatted with `{:?}`, and wraps a +/// `Debug` detail the same way. The original value is not kept. pub struct DebugEyreError; -#[cgp_provider] -impl ErrorRaiser for DebugEyreError +#[cgp_impl(DebugEyreError)] +#[use_type(HasErrorType.{Error = eyre::Report})] +impl ErrorRaiser where - Context: HasErrorType, E: Debug, { fn raise_error(e: E) -> Error { - eyre!("{:?}", e) + eyre!("{e:?}") } } -#[cgp_provider] -impl ErrorWrapper for DebugEyreError +#[cgp_impl(DebugEyreError)] +#[use_type(HasErrorType.{Error = eyre::Report})] +impl ErrorWrapper where - Context: HasErrorType, Detail: Debug, { fn wrap_error(error: Error, detail: Detail) -> Error { diff --git a/crates/standalone/error/cgp-error-eyre/src/impls/display_error.rs b/crates/standalone/error/cgp-error-eyre/src/impls/display_error.rs index abdb65b6..e4b42143 100644 --- a/crates/standalone/error/cgp-error-eyre/src/impls/display_error.rs +++ b/crates/standalone/error/cgp-error-eyre/src/impls/display_error.rs @@ -3,14 +3,16 @@ use core::fmt::Display; use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; -use eyre::{Error, eyre}; +use eyre::eyre; +/// Raises any `Display` value into [`eyre::Report`] as a message formatted with `{}`, and wraps a +/// `Display` detail the same way. The original value is not kept. pub struct DisplayEyreError; -#[cgp_provider] -impl ErrorRaiser for DisplayEyreError +#[cgp_impl(DisplayEyreError)] +#[use_type(HasErrorType.{Error = eyre::Report})] +impl ErrorRaiser where - Context: HasErrorType, E: Display, { fn raise_error(e: E) -> Error { @@ -18,10 +20,10 @@ where } } -#[cgp_provider] -impl ErrorWrapper for DisplayEyreError +#[cgp_impl(DisplayEyreError)] +#[use_type(HasErrorType.{Error = eyre::Report})] +impl ErrorWrapper where - Context: HasErrorType, Detail: Display, { fn wrap_error(error: Error, detail: Detail) -> Error { diff --git a/crates/standalone/error/cgp-error-eyre/src/impls/raise_eyre_error.rs b/crates/standalone/error/cgp-error-eyre/src/impls/raise_eyre_error.rs index 4c567e08..f62032f0 100644 --- a/crates/standalone/error/cgp-error-eyre/src/impls/raise_eyre_error.rs +++ b/crates/standalone/error/cgp-error-eyre/src/impls/raise_eyre_error.rs @@ -3,14 +3,15 @@ use core::fmt::Display; use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; -use eyre::Error; +/// Raises a standard error into [`eyre::Report`] without formatting it, so the source stays +/// available to `downcast_ref` and to the error chain, and wraps a detail with `wrap_err`. pub struct RaiseEyreError; -#[cgp_provider] -impl ErrorRaiser for RaiseEyreError +#[cgp_impl(RaiseEyreError)] +#[use_type(HasErrorType.{Error = eyre::Report})] +impl ErrorRaiser where - Context: HasErrorType, E: StdError + Send + Sync + 'static, { fn raise_error(e: E) -> Error { @@ -18,10 +19,10 @@ where } } -#[cgp_provider] -impl ErrorWrapper for RaiseEyreError +#[cgp_impl(RaiseEyreError)] +#[use_type(HasErrorType.{Error = eyre::Report})] +impl ErrorWrapper where - Context: HasErrorType, Detail: Display + Send + Sync + 'static, { fn wrap_error(error: Error, detail: Detail) -> Error { diff --git a/crates/standalone/error/cgp-error-eyre/src/impls/use_eyre_error.rs b/crates/standalone/error/cgp-error-eyre/src/impls/use_eyre_error.rs index c8210185..3a8b0b19 100644 --- a/crates/standalone/error/cgp-error-eyre/src/impls/use_eyre_error.rs +++ b/crates/standalone/error/cgp-error-eyre/src/impls/use_eyre_error.rs @@ -1,8 +1,10 @@ use cgp::error::{ErrorTypeProvider, ErrorTypeProviderComponent}; use cgp::prelude::*; -use eyre::Error; -#[cgp_new_provider] -impl ErrorTypeProvider for UseEyreError { - type Error = Error; +/// Sets the context's abstract error type to [`eyre::Report`]. +pub struct UseEyreError; + +#[cgp_impl(UseEyreError)] +impl ErrorTypeProvider { + type Error = eyre::Report; } diff --git a/crates/standalone/error/cgp-error-eyre/src/lib.rs b/crates/standalone/error/cgp-error-eyre/src/lib.rs index 2d7d9904..1116786f 100644 --- a/crates/standalone/error/cgp-error-eyre/src/lib.rs +++ b/crates/standalone/error/cgp-error-eyre/src/lib.rs @@ -1,5 +1,5 @@ -#![no_std] - +#![doc = include_str!("../README.md")] +// eyre needs `std`, so unlike the other backends this crate is not `no_std`. extern crate alloc; mod impls; diff --git a/crates/standalone/error/cgp-error-std/Cargo.toml b/crates/standalone/error/cgp-error-std/Cargo.toml index 3252d47a..05daad08 100644 --- a/crates/standalone/error/cgp-error-std/Cargo.toml +++ b/crates/standalone/error/cgp-error-std/Cargo.toml @@ -1,15 +1,16 @@ [package] name = "cgp-error-std" version = "0.8.0-alpha" +readme = "./README.md" edition = { workspace = true } license = { workspace = true } repository = { workspace = true } authors = { workspace = true } rust-version = { workspace = true } +homepage = { workspace = true } keywords = { workspace = true } -description = """ - Context-generic programming error handlers implemented using `core::error::Error` -""" +categories = { workspace = true } +description = "CGP error providers that make a boxed core::error::Error a context's abstract error type." [dependencies] cgp = { version = "0.8.0-alpha", path = "../../../main/cgp-core", package = "cgp-core" } diff --git a/crates/standalone/error/cgp-error-std/README.md b/crates/standalone/error/cgp-error-std/README.md new file mode 100644 index 00000000..039d38f5 --- /dev/null +++ b/crates/standalone/error/cgp-error-std/README.md @@ -0,0 +1,47 @@ +# cgp-error-std + +`cgp-error-std` makes a boxed standard error, `Box`, the +abstract error type of a [CGP](https://contextgeneric.dev) context, and supplies the providers that +raise errors into it and add context to it. It depends only on `cgp` and `alloc`, so it suits a +`no_std` context that still wants an open-ended error type. + +| Item | Wire it to | What it does | +|---|---|---| +| `UseBoxedStdError` | `ErrorTypeProviderComponent` | sets the context's `Error` to `cgp_error_std::Error` | +| `RaiseBoxedStdError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | boxes a standard error without formatting it, so `downcast_ref` still finds it; wraps a `Display` detail in a `WrapError` | +| `DebugBoxedStdError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | raises any `Debug` value as a `StringError` formatted with `{:?}`, and wraps a `Debug` detail in a `WrapError` | +| `DisplayBoxedStdError` | `ErrorRaiserComponent`, `ErrorWrapperComponent` | raises any `Display` value as a `StringError` formatted with `{}`, and wraps a `Display` detail in a `WrapError` | +| `Error` | — | the boxed error type itself | +| `StringError` | — | a standard error holding only a message | +| `WrapError` | — | a standard error holding a detail message and the error it wraps, returned as its `source` | + +A context usually routes each source type to the provider that suits it: + +```rust,ignore +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_std::{DisplayBoxedStdError, RaiseBoxedStdError, UseBoxedStdError}; + +pub struct App; + +delegate_components! { + App { + open ErrorRaiserComponent; + + ErrorTypeProviderComponent: UseBoxedStdError, + @ErrorRaiserComponent.std::io::Error: RaiseBoxedStdError, + @ErrorRaiserComponent.String: DisplayBoxedStdError, + ErrorWrapperComponent: RaiseBoxedStdError, + } +} + +let error = App::raise_error(std::io::Error::other("disk full")); +let error = App::wrap_error(error, "while saving"); +assert_eq!(format!("{error}"), "while saving"); +assert_eq!(format!("{error:#}"), "while saving: disk full"); +``` + +A `WrapError` prints its detail alone with `{}`, so a reporter that walks `source()` prints each +message once, and prints the whole chain with `{:#}` or `{:?}`. A `String` needs +`DisplayBoxedStdError` or `DebugBoxedStdError`, because it is not a standard error, and +`DebugBoxedStdError` prints it with quotes. diff --git a/crates/standalone/error/cgp-error-std/src/impls/debug_error.rs b/crates/standalone/error/cgp-error-std/src/impls/debug_error.rs index 8b432f8a..4be7849b 100644 --- a/crates/standalone/error/cgp-error-std/src/impls/debug_error.rs +++ b/crates/standalone/error/cgp-error-std/src/impls/debug_error.rs @@ -5,15 +5,16 @@ use core::fmt::Debug; use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; -use crate::WrapError; -use crate::types::{Error, StringError}; +use crate::{StringError, WrapError}; +/// Raises any `Debug` value as a [`StringError`] formatted with `{:?}`, and wraps a `Debug` detail +/// in a [`WrapError`] the same way. The original value is not kept. pub struct DebugBoxedStdError; -#[cgp_provider(ErrorRaiserComponent)] -impl ErrorRaiser for DebugBoxedStdError +#[cgp_impl(DebugBoxedStdError)] +#[use_type(HasErrorType.{Error = crate::Error})] +impl ErrorRaiser where - Context: HasErrorType, E: Debug, { fn raise_error(e: E) -> Error { @@ -21,10 +22,10 @@ where } } -#[cgp_provider(ErrorWrapperComponent)] -impl ErrorWrapper for DebugBoxedStdError +#[cgp_impl(DebugBoxedStdError)] +#[use_type(HasErrorType.{Error = crate::Error})] +impl ErrorWrapper where - Context: HasErrorType, Detail: Debug, { fn wrap_error(error: Error, detail: Detail) -> Error { diff --git a/crates/standalone/error/cgp-error-std/src/impls/display_error.rs b/crates/standalone/error/cgp-error-std/src/impls/display_error.rs index f532b422..8cf3b574 100644 --- a/crates/standalone/error/cgp-error-std/src/impls/display_error.rs +++ b/crates/standalone/error/cgp-error-std/src/impls/display_error.rs @@ -2,20 +2,19 @@ use alloc::boxed::Box; use alloc::string::ToString; use core::fmt::Display; -use cgp::error::{ - ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent, HasErrorType, -}; +use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; -use crate::WrapError; -use crate::types::{Error, StringError}; +use crate::{StringError, WrapError}; +/// Raises any `Display` value as a [`StringError`] formatted with `{}`, and wraps a `Display` +/// detail in a [`WrapError`] the same way. The original value is not kept. pub struct DisplayBoxedStdError; -#[cgp_provider(ErrorRaiserComponent)] -impl ErrorRaiser for DisplayBoxedStdError +#[cgp_impl(DisplayBoxedStdError)] +#[use_type(HasErrorType.{Error = crate::Error})] +impl ErrorRaiser where - Context: HasErrorType, E: Display, { fn raise_error(e: E) -> Error { @@ -23,10 +22,10 @@ where } } -#[cgp_provider(ErrorWrapperComponent)] -impl ErrorWrapper for DisplayBoxedStdError +#[cgp_impl(DisplayBoxedStdError)] +#[use_type(HasErrorType.{Error = crate::Error})] +impl ErrorWrapper where - Context: HasErrorType, Detail: Display, { fn wrap_error(error: Error, detail: Detail) -> Error { diff --git a/crates/standalone/error/cgp-error-std/src/impls/raise_boxed.rs b/crates/standalone/error/cgp-error-std/src/impls/raise_boxed.rs index 6f9804ad..37a0816a 100644 --- a/crates/standalone/error/cgp-error-std/src/impls/raise_boxed.rs +++ b/crates/standalone/error/cgp-error-std/src/impls/raise_boxed.rs @@ -1,19 +1,38 @@ +use alloc::boxed::Box; +use alloc::string::ToString; use core::error::Error as StdError; +use core::fmt::Display; -use cgp::error::{ErrorRaiser, ErrorRaiserComponent}; +use cgp::error::{ErrorRaiser, ErrorRaiserComponent, ErrorWrapper, ErrorWrapperComponent}; use cgp::prelude::*; -use crate::types::Error; +use crate::WrapError; +/// Boxes a standard error without formatting it, so the source stays available to `downcast_ref` +/// and to the error chain, and wraps a `Display` detail in a [`WrapError`]. pub struct RaiseBoxedStdError; -#[cgp_provider(ErrorRaiserComponent)] -impl ErrorRaiser for RaiseBoxedStdError +#[cgp_impl(RaiseBoxedStdError)] +#[use_type(HasErrorType.{Error = crate::Error})] +impl ErrorRaiser where - Context: HasErrorType, E: StdError + Send + Sync + 'static, { fn raise_error(e: E) -> Error { - e.into() + Box::new(e) + } +} + +#[cgp_impl(RaiseBoxedStdError)] +#[use_type(HasErrorType.{Error = crate::Error})] +impl ErrorWrapper +where + Detail: Display, +{ + fn wrap_error(error: Error, detail: Detail) -> Error { + Box::new(WrapError { + detail: detail.to_string(), + source: error, + }) } } diff --git a/crates/standalone/error/cgp-error-std/src/impls/use_boxed.rs b/crates/standalone/error/cgp-error-std/src/impls/use_boxed.rs index 56de0f2d..9144a56f 100644 --- a/crates/standalone/error/cgp-error-std/src/impls/use_boxed.rs +++ b/crates/standalone/error/cgp-error-std/src/impls/use_boxed.rs @@ -1,11 +1,10 @@ use cgp::error::{ErrorTypeProvider, ErrorTypeProviderComponent}; use cgp::prelude::*; -use crate::types::Error; - +/// Sets the context's abstract error type to [`Error`](crate::Error), a boxed standard error. pub struct UseBoxedStdError; -#[cgp_provider(ErrorTypeProviderComponent)] -impl ErrorTypeProvider for UseBoxedStdError { - type Error = Error; +#[cgp_impl(UseBoxedStdError)] +impl ErrorTypeProvider { + type Error = crate::Error; } diff --git a/crates/standalone/error/cgp-error-std/src/lib.rs b/crates/standalone/error/cgp-error-std/src/lib.rs index b5897d15..31f96ebc 100644 --- a/crates/standalone/error/cgp-error-std/src/lib.rs +++ b/crates/standalone/error/cgp-error-std/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] #![no_std] extern crate alloc; diff --git a/crates/standalone/error/cgp-error-std/src/types/error.rs b/crates/standalone/error/cgp-error-std/src/types/error.rs index 728c33f6..dd820db6 100644 --- a/crates/standalone/error/cgp-error-std/src/types/error.rs +++ b/crates/standalone/error/cgp-error-std/src/types/error.rs @@ -1,4 +1,6 @@ use alloc::boxed::Box; use core::error::Error as StdError; +/// The abstract error type [`UseBoxedStdError`](crate::UseBoxedStdError) selects: any standard +/// error, boxed. pub type Error = Box; diff --git a/crates/standalone/error/cgp-error-std/src/types/string.rs b/crates/standalone/error/cgp-error-std/src/types/string.rs index 3a256530..79ffd47a 100644 --- a/crates/standalone/error/cgp-error-std/src/types/string.rs +++ b/crates/standalone/error/cgp-error-std/src/types/string.rs @@ -2,6 +2,8 @@ use alloc::string::String; use core::error::Error; use core::fmt::{Debug, Display}; +/// A standard error that carries only a message, produced when a provider formats a value that is +/// not itself a standard error. pub struct StringError { pub message: String, } diff --git a/crates/standalone/error/cgp-error-std/src/types/wrap.rs b/crates/standalone/error/cgp-error-std/src/types/wrap.rs index 4e8675cb..ba85437c 100644 --- a/crates/standalone/error/cgp-error-std/src/types/wrap.rs +++ b/crates/standalone/error/cgp-error-std/src/types/wrap.rs @@ -1,23 +1,46 @@ use alloc::string::String; use core::error::Error as StdError; -use core::fmt::{Debug, Display}; +use core::fmt::{Debug, Display, Formatter, Result}; use crate::Error; +/// A standard error that adds a detail message to the error it wraps, which it returns as its +/// `source`. +/// +/// `{}` prints the detail alone, so a reporter that walks `source()` prints each message once. +/// `{:#}` and `{:?}` print the whole chain, joined by `": "`. pub struct WrapError { pub detail: String, pub source: Error, } +impl WrapError { + fn fmt_chain(&self, f: &mut Formatter<'_>) -> Result { + f.write_str(&self.detail)?; + + let mut source: Option<&(dyn StdError + 'static)> = Some(self.source.as_ref()); + while let Some(error) = source { + write!(f, ": {error}")?; + source = error.source(); + } + + Ok(()) + } +} + impl Display for WrapError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}: {}", self.detail, self.source) + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + if f.alternate() { + self.fmt_chain(f) + } else { + f.write_str(&self.detail) + } } } impl Debug for WrapError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - Display::fmt(self, f) + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + self.fmt_chain(f) } } diff --git a/crates/tests/README.md b/crates/tests/README.md index 22ee1392..7ea9eb0a 100644 --- a/crates/tests/README.md +++ b/crates/tests/README.md @@ -40,8 +40,8 @@ is the reference example of the layout. The concept targets currently cover: basic delegation, impl-side dependencies, implicit arguments, higher-order providers, generic components, abstract types, getters, field access, extensible records, extensible variants, checking, -dispatching, namespaces, handlers, monadic handlers, async and Send bounds, and -blanket traits. The set grows and subdivides over time. `cgp-macro-tests` follows +dispatching, namespaces, handlers, monadic handlers, async and Send bounds, +blanket traits, and the standalone error backends (`error_backends`). The set grows and subdivides over time. `cgp-macro-tests` follows the same shape, with `ident_with_type_params` for parser corner cases and the failure-case targets `parser_rejections` and `invalid_expansion`. diff --git a/crates/tests/cgp-tests/Cargo.toml b/crates/tests/cgp-tests/Cargo.toml index d1fe9fe4..b4c3fb20 100644 --- a/crates/tests/cgp-tests/Cargo.toml +++ b/crates/tests/cgp-tests/Cargo.toml @@ -19,6 +19,9 @@ insta = { version = "1.48.0" } futures = { version = "0.3.31" } [dev-dependencies] +cgp-error-anyhow = { workspace = true } +cgp-error-eyre = { workspace = true } +cgp-error-std = { workspace = true } cgp-macro-core = { workspace = true } syn = { version = "2.0.95", features = [ "full", "extra-traits" ] } quote = { version = "1.0.38" } diff --git a/crates/tests/cgp-tests/tests/error_backends/anyhow_formatting.rs b/crates/tests/cgp-tests/tests/error_backends/anyhow_formatting.rs new file mode 100644 index 00000000..cc9595ff --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/anyhow_formatting.rs @@ -0,0 +1,57 @@ +//! `DebugAnyhowError` and `DisplayAnyhowError` raise any `Debug` or `Display` value, including a +//! `String` that is not a standard error, by formatting it into a new anyhow message, and wrap a +//! detail the same way. The original value is not kept, and `{:?}` quotes a string. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-anyhow/testing.md. + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_anyhow::{DebugAnyhowError, DisplayAnyhowError, UseAnyhowError}; + +#[derive(Debug)] +pub struct Rejected { + pub code: u32, +} + +pub struct App; + +delegate_components! { + App { + open { ErrorRaiserComponent, ErrorWrapperComponent }; + + ErrorTypeProviderComponent: UseAnyhowError, + @ErrorRaiserComponent.Rejected: DebugAnyhowError, + @ErrorRaiserComponent.String: DisplayAnyhowError, + @ErrorRaiserComponent.&'static str: DebugAnyhowError, + @ErrorWrapperComponent.String: DisplayAnyhowError, + @ErrorWrapperComponent.u32: DebugAnyhowError, + } +} + +check_components! { + App { + ErrorRaiserComponent: [Rejected, String, &'static str], + ErrorWrapperComponent: [String, u32], + } +} + +#[test] +fn test_anyhow_formatting() { + let error = App::raise_error(Rejected { code: 7 }); + assert_eq!(format!("{error}"), "Rejected { code: 7 }"); + // The report holds only the formatted message, with no source behind it. + assert_eq!(error.chain().count(), 1); + + let error = App::raise_error(String::from("plain message")); + assert_eq!(format!("{error}"), "plain message"); + + let error = App::raise_error("quoted message"); + assert_eq!(format!("{error}"), "\"quoted message\""); + + let error = App::wrap_error(error, 42u32); + let error = App::wrap_error(error, String::from("while checking")); + assert_eq!( + format!("{error:#}"), + "while checking: 42: \"quoted message\"" + ); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/anyhow_raise_and_wrap.rs b/crates/tests/cgp-tests/tests/error_backends/anyhow_raise_and_wrap.rs new file mode 100644 index 00000000..9ec8927f --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/anyhow_raise_and_wrap.rs @@ -0,0 +1,50 @@ +//! `RaiseAnyhowError` raises a standard error into `anyhow::Error` without formatting it, so the +//! source is still found by `downcast_ref`, and as a wrapper it adds a `'static` detail as anyhow +//! context: `{}` prints the outermost detail and `{:#}` the whole chain. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-anyhow/testing.md. + +use std::io; + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_anyhow::{Error, RaiseAnyhowError, UseAnyhowError}; + +pub struct App; + +delegate_components! { + App { + ErrorTypeProviderComponent: UseAnyhowError, + ErrorRaiserComponent: RaiseAnyhowError, + ErrorWrapperComponent: RaiseAnyhowError, + } +} + +check_components! { + App { + ErrorTypeProviderComponent, + ErrorRaiserComponent: io::Error, + ErrorWrapperComponent: [&'static str, String], + } +} + +#[test] +fn test_anyhow_raise_and_wrap() { + let error: Error = App::raise_error(io::Error::new(io::ErrorKind::NotFound, "no file")); + assert_eq!( + error.downcast_ref::().map(io::Error::kind), + Some(io::ErrorKind::NotFound) + ); + + let error = App::wrap_error(error, "while loading"); + let error = App::wrap_error(error, String::from("while starting")); + + assert_eq!(format!("{error}"), "while starting"); + assert_eq!( + format!("{error:#}"), + "while starting: while loading: no file" + ); + + // Wrapping keeps the original error reachable through the chain. + assert!(error.downcast_ref::().is_some()); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/eyre_formatting.rs b/crates/tests/cgp-tests/tests/error_backends/eyre_formatting.rs new file mode 100644 index 00000000..77cfc415 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/eyre_formatting.rs @@ -0,0 +1,47 @@ +//! `DebugEyreError` and `DisplayEyreError` raise any `Debug` or `Display` value by formatting it +//! into a new eyre report, and wrap a detail the same way. The original value is not kept. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-eyre/testing.md. + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_eyre::{DebugEyreError, DisplayEyreError, UseEyreError}; + +#[derive(Debug)] +pub struct Rejected { + pub code: u32, +} + +pub struct App; + +delegate_components! { + App { + open { ErrorRaiserComponent, ErrorWrapperComponent }; + + ErrorTypeProviderComponent: UseEyreError, + @ErrorRaiserComponent.Rejected: DebugEyreError, + @ErrorRaiserComponent.String: DisplayEyreError, + @ErrorWrapperComponent.String: DisplayEyreError, + @ErrorWrapperComponent.u32: DebugEyreError, + } +} + +check_components! { + App { + ErrorRaiserComponent: [Rejected, String], + ErrorWrapperComponent: [String, u32], + } +} + +#[test] +fn test_eyre_formatting() { + let error = App::raise_error(Rejected { code: 7 }); + assert_eq!(format!("{error}"), "Rejected { code: 7 }"); + // The report holds only the formatted message, with no source behind it. + assert_eq!(error.chain().count(), 1); + + let error = App::raise_error(String::from("plain message")); + let error = App::wrap_error(error, 42u32); + let error = App::wrap_error(error, String::from("while checking")); + assert_eq!(format!("{error:#}"), "while checking: 42: plain message"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/eyre_raise_and_wrap.rs b/crates/tests/cgp-tests/tests/error_backends/eyre_raise_and_wrap.rs new file mode 100644 index 00000000..1ca3e0f8 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/eyre_raise_and_wrap.rs @@ -0,0 +1,59 @@ +//! `RaiseEyreError` raises a standard error into `eyre::Report` without formatting it, and as a +//! wrapper adds a `'static` detail with `wrap_err`. No test in this target installs an eyre hook, +//! so building a report here also checks that the crate turns on eyre's `auto-install` feature. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-eyre/testing.md. + +use std::io; + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_eyre::{Error, RaiseEyreError, UseEyreError}; + +pub struct App; + +delegate_components! { + App { + ErrorTypeProviderComponent: UseEyreError, + ErrorRaiserComponent: RaiseEyreError, + ErrorWrapperComponent: RaiseEyreError, + } +} + +check_components! { + App { + ErrorTypeProviderComponent, + ErrorRaiserComponent: io::Error, + ErrorWrapperComponent: [&'static str, String], + } +} + +#[test] +fn test_eyre_raise_and_wrap() { + let error: Error = App::raise_error(io::Error::new(io::ErrorKind::NotFound, "no file")); + assert_eq!( + error.downcast_ref::().map(io::Error::kind), + Some(io::ErrorKind::NotFound) + ); + + let error = App::wrap_error(error, "while loading"); + let error = App::wrap_error(error, String::from("while starting")); + + assert_eq!(format!("{error}"), "while starting"); + assert_eq!( + format!("{error:#}"), + "while starting: while loading: no file" + ); + assert!(error.downcast_ref::().is_some()); + + // The default handler prints the chain, then a backtrace section when `RUST_BACKTRACE` or + // `RUST_LIB_BACKTRACE` is set, so only the chain is matched exactly. The crate leaves eyre's + // `track-caller` feature off, since the recorded location would be a line inside the backend, + // so no `Location:` section appears. + let debug = format!("{error:?}"); + assert!( + debug.starts_with("while starting\n\nCaused by:\n 0: while loading\n 1: no file"), + "unexpected report: {debug}" + ); + assert!(!debug.contains("Location:"), "unexpected report: {debug}"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/generic_equivalents.rs b/crates/tests/cgp-tests/tests/error_backends/generic_equivalents.rs new file mode 100644 index 00000000..ece57e27 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/generic_equivalents.rs @@ -0,0 +1,38 @@ +//! The generic `UseType` and `RaiseFrom` do the same job as `UseAnyhowError` and +//! `RaiseAnyhowError`'s raiser, since both convert through `From`. What a backend adds over the +//! generic providers is its wrappers and its formatting raisers. `RaiseFrom` also re-raises a value +//! that is already the context's error, through the reflexive `From for T`, which the backend's +//! raiser cannot do because `anyhow::Error` is not itself a standard error. +//! +//! See cgp-knowledge-base/projects/error/guides/choosing-a-backend.md. + +use std::io; + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent}; +use cgp::extra::error::RaiseFrom; +use cgp::prelude::*; + +pub struct App; + +delegate_components! { + App { + ErrorTypeProviderComponent: UseType, + ErrorRaiserComponent: RaiseFrom, + } +} + +check_components! { + App { + ErrorRaiserComponent: [io::Error, cgp_error_anyhow::Error], + } +} + +#[test] +fn test_generic_equivalents() { + let error: cgp_error_anyhow::Error = App::raise_error(io::Error::other("disk full")); + assert!(error.downcast_ref::().is_some()); + assert_eq!(format!("{error}"), "disk full"); + + let error: cgp_error_anyhow::Error = App::raise_error(error); + assert!(error.downcast_ref::().is_some()); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/mod.rs b/crates/tests/cgp-tests/tests/error_backends/mod.rs new file mode 100644 index 00000000..11d4cf7e --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/mod.rs @@ -0,0 +1,17 @@ +//! One unit test per file. Each file is self-contained: it defines its own +//! contexts and wiring at module scope so that the type-level wiring of one test +//! never leaks into another. + +pub mod anyhow_formatting; +pub mod anyhow_raise_and_wrap; +pub mod eyre_formatting; +pub mod eyre_raise_and_wrap; +pub mod generic_equivalents; +pub mod namespace_wiring; +pub mod readme_anyhow; +pub mod readme_eyre; +pub mod readme_std; +pub mod std_formatting; +pub mod std_raise_and_wrap; +pub mod std_wrap_error; +pub mod swapping_backends; diff --git a/crates/tests/cgp-tests/tests/error_backends/namespace_wiring.rs b/crates/tests/cgp-tests/tests/error_backends/namespace_wiring.rs new file mode 100644 index 00000000..42a4a594 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/namespace_wiring.rs @@ -0,0 +1,41 @@ +//! A context that joins `DefaultNamespace` wires a backend through the full paths the error +//! components register under, `@cgp.core.error.*`, dispatching raisers per source type. +//! +//! See cgp-knowledge-base/projects/error/guides/choosing-a-backend.md. + +use std::io; + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_anyhow::{DisplayAnyhowError, RaiseAnyhowError, UseAnyhowError}; + +pub struct App; + +delegate_components! { + App { + namespace DefaultNamespace; + + @cgp.core.error.ErrorTypeProviderComponent: UseAnyhowError, + @cgp.core.error.ErrorRaiserComponent.io::Error: RaiseAnyhowError, + @cgp.core.error.ErrorRaiserComponent.String: DisplayAnyhowError, + @cgp.core.error.ErrorWrapperComponent.&'static str: RaiseAnyhowError, + } +} + +check_components! { + App { + ErrorTypeProviderComponent, + ErrorRaiserComponent: [io::Error, String], + ErrorWrapperComponent: &'static str, + } +} + +#[test] +fn test_namespace_wiring() { + let error = App::raise_error(io::Error::other("disk full")); + let error = App::wrap_error(error, "while saving"); + assert_eq!(format!("{error:#}"), "while saving: disk full"); + + let error = App::raise_error(String::from("bad input")); + assert_eq!(format!("{error}"), "bad input"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/readme_anyhow.rs b/crates/tests/cgp-tests/tests/error_backends/readme_anyhow.rs new file mode 100644 index 00000000..2a3d9034 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/readme_anyhow.rs @@ -0,0 +1,29 @@ +//! The wiring example in `cgp-error-anyhow`'s README, copied verbatim so that CI compiles and runs it. +//! The README marks the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`; +//! keep the two in sync when either changes. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-anyhow/testing.md. + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_anyhow::{DisplayAnyhowError, RaiseAnyhowError, UseAnyhowError}; + +pub struct App; + +delegate_components! { + App { + open ErrorRaiserComponent; + + ErrorTypeProviderComponent: UseAnyhowError, + @ErrorRaiserComponent.std::io::Error: RaiseAnyhowError, + @ErrorRaiserComponent.String: DisplayAnyhowError, + ErrorWrapperComponent: RaiseAnyhowError, + } +} + +#[test] +fn test_readme_anyhow() { + let error = App::raise_error(std::io::Error::other("disk full")); + let error = App::wrap_error(error, "while saving"); + assert_eq!(format!("{error:#}"), "while saving: disk full"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/readme_eyre.rs b/crates/tests/cgp-tests/tests/error_backends/readme_eyre.rs new file mode 100644 index 00000000..a410724c --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/readme_eyre.rs @@ -0,0 +1,29 @@ +//! The wiring example in `cgp-error-eyre`'s README, copied verbatim so that CI compiles and runs it. +//! The README marks the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`; +//! keep the two in sync when either changes. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-eyre/testing.md. + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_eyre::{DisplayEyreError, RaiseEyreError, UseEyreError}; + +pub struct App; + +delegate_components! { + App { + open ErrorRaiserComponent; + + ErrorTypeProviderComponent: UseEyreError, + @ErrorRaiserComponent.std::io::Error: RaiseEyreError, + @ErrorRaiserComponent.String: DisplayEyreError, + ErrorWrapperComponent: RaiseEyreError, + } +} + +#[test] +fn test_readme_eyre() { + let error = App::raise_error(std::io::Error::other("disk full")); + let error = App::wrap_error(error, "while saving"); + assert_eq!(format!("{error:#}"), "while saving: disk full"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/readme_std.rs b/crates/tests/cgp-tests/tests/error_backends/readme_std.rs new file mode 100644 index 00000000..b95de4d4 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/readme_std.rs @@ -0,0 +1,30 @@ +//! The wiring example in `cgp-error-std`'s README, copied verbatim so that CI compiles and runs it. +//! The README marks the block `ignore` because in the crate's own doctests `cgp` names `cgp-core`; +//! keep the two in sync when either changes. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-std/testing.md. + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_std::{DisplayBoxedStdError, RaiseBoxedStdError, UseBoxedStdError}; + +pub struct App; + +delegate_components! { + App { + open ErrorRaiserComponent; + + ErrorTypeProviderComponent: UseBoxedStdError, + @ErrorRaiserComponent.std::io::Error: RaiseBoxedStdError, + @ErrorRaiserComponent.String: DisplayBoxedStdError, + ErrorWrapperComponent: RaiseBoxedStdError, + } +} + +#[test] +fn test_readme_std() { + let error = App::raise_error(std::io::Error::other("disk full")); + let error = App::wrap_error(error, "while saving"); + assert_eq!(format!("{error}"), "while saving"); + assert_eq!(format!("{error:#}"), "while saving: disk full"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/std_formatting.rs b/crates/tests/cgp-tests/tests/error_backends/std_formatting.rs new file mode 100644 index 00000000..982716c2 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/std_formatting.rs @@ -0,0 +1,49 @@ +//! `DebugBoxedStdError` and `DisplayBoxedStdError` raise any `Debug` or `Display` value as a +//! `StringError` holding the formatted message, and wrap a detail in a `WrapError` the same way. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-std/testing.md. + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_std::{DebugBoxedStdError, DisplayBoxedStdError, StringError, UseBoxedStdError}; + +#[derive(Debug)] +pub struct Rejected { + pub code: u32, +} + +pub struct App; + +delegate_components! { + App { + open { ErrorRaiserComponent, ErrorWrapperComponent }; + + ErrorTypeProviderComponent: UseBoxedStdError, + @ErrorRaiserComponent.Rejected: DebugBoxedStdError, + @ErrorRaiserComponent.String: DisplayBoxedStdError, + @ErrorWrapperComponent.String: DisplayBoxedStdError, + @ErrorWrapperComponent.u32: DebugBoxedStdError, + } +} + +check_components! { + App { + ErrorRaiserComponent: [Rejected, String], + ErrorWrapperComponent: [String, u32], + } +} + +#[test] +fn test_std_formatting() { + let error = App::raise_error(Rejected { code: 7 }); + assert_eq!( + error.downcast_ref::().unwrap().message, + "Rejected { code: 7 }" + ); + + let error = App::raise_error(String::from("plain message")); + let error = App::wrap_error(error, 42u32); + let error = App::wrap_error(error, String::from("while checking")); + assert_eq!(format!("{error}"), "while checking"); + assert_eq!(format!("{error:#}"), "while checking: 42: plain message"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/std_raise_and_wrap.rs b/crates/tests/cgp-tests/tests/error_backends/std_raise_and_wrap.rs new file mode 100644 index 00000000..2c387966 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/std_raise_and_wrap.rs @@ -0,0 +1,53 @@ +//! `RaiseBoxedStdError` boxes a standard error without formatting it, so the source is still found +//! by `downcast_ref`, and as a wrapper puts a `Display` detail in a `WrapError`. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-std/testing.md. + +use std::io; + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; +use cgp_error_std::{Error, RaiseBoxedStdError, UseBoxedStdError, WrapError}; + +pub struct App; + +delegate_components! { + App { + ErrorTypeProviderComponent: UseBoxedStdError, + ErrorRaiserComponent: RaiseBoxedStdError, + ErrorWrapperComponent: RaiseBoxedStdError, + } +} + +check_components! { + App { + ErrorTypeProviderComponent, + ErrorRaiserComponent: io::Error, + ErrorWrapperComponent: [&'static str, String, u32], + } +} + +#[test] +fn test_std_raise_and_wrap() { + let error: Error = App::raise_error(io::Error::new(io::ErrorKind::NotFound, "no file")); + assert_eq!( + error.downcast_ref::().map(io::Error::kind), + Some(io::ErrorKind::NotFound) + ); + + let error = App::wrap_error(error, "while loading"); + let error = App::wrap_error(error, String::from("while starting")); + + assert_eq!(format!("{error}"), "while starting"); + assert_eq!( + format!("{error:#}"), + "while starting: while loading: no file" + ); + assert_eq!( + format!("{error:?}"), + "while starting: while loading: no file" + ); + + let outer = error.downcast_ref::().unwrap(); + assert_eq!(outer.detail, "while starting"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/std_wrap_error.rs b/crates/tests/cgp-tests/tests/error_backends/std_wrap_error.rs new file mode 100644 index 00000000..ed161b0d --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/std_wrap_error.rs @@ -0,0 +1,42 @@ +//! `WrapError` prints its detail alone with `{}` and returns the wrapped error as its `source`, so +//! walking the chain prints each message exactly once; `{:#}` and `{:?}` print the whole chain. +//! `StringError` prints its message unquoted for both `{}` and `{:?}`. +//! +//! See cgp-knowledge-base/projects/error/cgp-error-std/testing.md. + +use core::error::Error as _; + +use cgp_error_std::{Error, StringError, WrapError}; + +#[test] +fn test_wrap_error_chain() { + let inner: Error = Box::new(StringError::from(String::from("inner"))); + let middle: Error = Box::new(WrapError { + detail: String::from("middle"), + source: inner, + }); + let outer = WrapError { + detail: String::from("outer"), + source: middle, + }; + + let mut messages = Vec::new(); + let mut current: Option<&(dyn core::error::Error + 'static)> = Some(&outer); + while let Some(error) = current { + messages.push(error.to_string()); + current = error.source(); + } + assert_eq!(messages, ["outer", "middle", "inner"]); + + assert_eq!(format!("{outer}"), "outer"); + assert_eq!(format!("{outer:#}"), "outer: middle: inner"); + assert_eq!(format!("{outer:?}"), "outer: middle: inner"); + assert!(outer.source().is_some()); +} + +#[test] +fn test_string_error_format() { + let error = StringError::from(String::from("message")); + assert_eq!(format!("{error}"), "message"); + assert_eq!(format!("{error:?}"), "message"); +} diff --git a/crates/tests/cgp-tests/tests/error_backends/swapping_backends.rs b/crates/tests/cgp-tests/tests/error_backends/swapping_backends.rs new file mode 100644 index 00000000..e6cf8584 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends/swapping_backends.rs @@ -0,0 +1,85 @@ +//! One provider written against `CanRaiseError` and `CanWrapError` runs unchanged on three +//! contexts that wire the three backends, and produces the same message from each. +//! +//! See cgp-knowledge-base/projects/error/architecture.md. + +use core::fmt::Display; +use core::num::ParseIntError; + +use cgp::core::error::{ErrorRaiserComponent, ErrorTypeProviderComponent, ErrorWrapperComponent}; +use cgp::prelude::*; + +#[cgp_component(PortParser)] +#[use_type(HasErrorType.Error)] +pub trait CanParsePort { + fn parse_port(&self, raw: &str) -> Result; +} + +#[cgp_impl(new ParseWithContext)] +#[uses(CanRaiseError, CanWrapError)] +#[use_type(HasErrorType.Error)] +impl PortParser { + fn parse_port(&self, raw: &str) -> Result { + raw.parse::() + .map_err(Self::raise_error) + .map_err(|e| Self::wrap_error(e, format!("invalid port {raw:?}"))) + } +} + +macro_rules! backend_app { + ($app:ident, $use:ty, $raise:ty) => { + pub struct $app; + + delegate_components! { + $app { + ErrorTypeProviderComponent: $use, + [ErrorRaiserComponent, ErrorWrapperComponent]: $raise, + PortParserComponent: ParseWithContext, + } + } + + check_components! { + $app { + PortParserComponent, + } + } + }; +} + +backend_app!( + AnyhowApp, + cgp_error_anyhow::UseAnyhowError, + cgp_error_anyhow::RaiseAnyhowError +); +backend_app!( + EyreApp, + cgp_error_eyre::UseEyreError, + cgp_error_eyre::RaiseEyreError +); +backend_app!( + StdApp, + cgp_error_std::UseBoxedStdError, + cgp_error_std::RaiseBoxedStdError +); + +fn assert_parses(app: &App) +where + App: CanParsePort, + App::Error: Display, +{ + assert_eq!(app.parse_port("8080").ok(), Some(8080)); + + let error = app.parse_port("80a").err().unwrap(); + assert_eq!(format!("{error}"), "invalid port \"80a\""); + assert_eq!( + format!("{error:#}"), + "invalid port \"80a\": invalid digit found in string" + ); +} + +#[test] +fn test_swapping_backends() { + assert_parses(&AnyhowApp); + assert_parses(&EyreApp); + assert_parses(&StdApp); +} diff --git a/crates/tests/cgp-tests/tests/error_backends_tests.rs b/crates/tests/cgp-tests/tests/error_backends_tests.rs new file mode 100644 index 00000000..10a7a129 --- /dev/null +++ b/crates/tests/cgp-tests/tests/error_backends_tests.rs @@ -0,0 +1,12 @@ +//! Entrypoint for the `error_backends` concept. +//! +//! Covers the standalone error backends `cgp-error-anyhow`, `cgp-error-eyre`, and +//! `cgp-error-std`: wiring each as a context's abstract error type, raising standard and +//! non-standard source errors through their providers, wrapping details, and the output each +//! produces. These crates are documented as a project of their own in the knowledge base. +//! +//! See cgp-knowledge-base/projects/error/README.md and +//! cgp-knowledge-base/cgp/concepts/modular-error-handling.md. +#![allow(dead_code)] + +pub mod error_backends;