From c1cd564c15edeca27c1f1ee2779d7e3ab18636a1 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Wed, 8 Jan 2025 19:38:51 +0000 Subject: [PATCH 1/4] Simplify error handling chapter using cgp-error-anyhow --- Cargo.lock | 36 +++++----- Cargo.toml | 36 +++++----- content/error-handling.md | 141 +++++++------------------------------- src/lib.rs | 1 + 4 files changed, 61 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f4de70..8401f2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,7 +101,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cgp" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-async", "cgp-core", @@ -111,7 +111,7 @@ dependencies = [ [[package]] name = "cgp-async" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-async-macro", "cgp-sync", @@ -120,7 +120,7 @@ dependencies = [ [[package]] name = "cgp-async-macro" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "proc-macro2", "quote", @@ -130,12 +130,12 @@ dependencies = [ [[package]] name = "cgp-component" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" [[package]] name = "cgp-component-macro" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-component-macro-lib", "syn", @@ -144,7 +144,7 @@ dependencies = [ [[package]] name = "cgp-component-macro-lib" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "itertools", "prettyplease", @@ -156,7 +156,7 @@ dependencies = [ [[package]] name = "cgp-core" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-async", "cgp-component", @@ -170,7 +170,7 @@ dependencies = [ [[package]] name = "cgp-error" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-async", "cgp-component", @@ -181,7 +181,7 @@ dependencies = [ [[package]] name = "cgp-error-anyhow" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "anyhow", "cgp-core", @@ -190,7 +190,7 @@ dependencies = [ [[package]] name = "cgp-error-extra" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-error", ] @@ -198,7 +198,7 @@ dependencies = [ [[package]] name = "cgp-extra" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-error-extra", "cgp-inner", @@ -209,7 +209,7 @@ dependencies = [ [[package]] name = "cgp-field" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-component", "cgp-type", @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "cgp-field-macro" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-field-macro-lib", "proc-macro2", @@ -227,7 +227,7 @@ dependencies = [ [[package]] name = "cgp-field-macro-lib" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "prettyplease", "proc-macro2", @@ -238,7 +238,7 @@ dependencies = [ [[package]] name = "cgp-inner" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-component", "cgp-component-macro", @@ -262,7 +262,7 @@ dependencies = [ [[package]] name = "cgp-run" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-async", "cgp-component", @@ -273,7 +273,7 @@ dependencies = [ [[package]] name = "cgp-runtime" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-core", ] @@ -290,7 +290,7 @@ dependencies = [ [[package]] name = "cgp-type" version = "0.2.0" -source = "git+https://github.com/contextgeneric/cgp.git?branch=getter-component#bda46a93e4e9ee550a3f191191cfabefe0f463fd" +source = "git+https://github.com/contextgeneric/cgp.git#c86ec514facca3e88470daa252bd066a0617ae9b" dependencies = [ "cgp-component", "cgp-component-macro", diff --git a/Cargo.toml b/Cargo.toml index 3949bd4..8358d79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,21 +16,21 @@ reqwest = { version = "0.12.12", features = [ "blocking", "json" ] } [patch.crates-io] -cgp = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-core = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-extra = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-async = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-async-macro = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-component = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-component-macro = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-component-macro-lib = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-type = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-field = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-field-macro = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-field-macro-lib = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-error = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-error-extra = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-error-anyhow = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-run = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-runtime = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } -cgp-inner = { git = "https://github.com/contextgeneric/cgp.git", branch = "getter-component" } +cgp = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-core = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-extra = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-async = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-async-macro = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-component = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-component-macro = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-component-macro-lib = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-type = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-field = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-field-macro = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-field-macro-lib = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-error = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-error-extra = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-error-anyhow = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-run = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-runtime = { git = "https://github.com/contextgeneric/cgp.git" } +cgp-inner = { git = "https://github.com/contextgeneric/cgp.git" } diff --git a/content/error-handling.md b/content/error-handling.md index 098eddd..ea64246 100644 --- a/content/error-handling.md +++ b/content/error-handling.md @@ -45,13 +45,7 @@ use core::fmt::Debug; use cgp::prelude::*; -#[cgp_component { - name: ErrorTypeComponent, - provider: ProvideErrorType, -}] -pub trait HasErrorType { - type Error: Debug; -} +cgp_type!( Error: Debug ); ``` The trait `HasErrorType` is quite special, in the sense that it serves as a standard @@ -73,21 +67,8 @@ to use the abstract error type provided by `HasErrorType`: # # use cgp::prelude::*; # -# #[cgp_component { -# name: TimeTypeComponent, -# provider: ProvideTimeType, -# }] -# pub trait HasTimeType { -# type Time; -# } -# -# #[cgp_component { -# name: AuthTokenTypeComponent, -# provider: ProvideAuthTokenType, -# }] -# pub trait HasAuthTokenType { -# type AuthToken; -# } +# cgp_type!( Time ); +# cgp_type!( AuthToken ); # #[cgp_component { provider: AuthTokenValidator, @@ -134,21 +115,8 @@ Using this technique, we can re-write `ValidateTokenIsNotExpired` to convert a s # # use cgp::prelude::*; # -# #[cgp_component { -# name: TimeTypeComponent, -# provider: ProvideTimeType, -# }] -# pub trait HasTimeType { -# type Time; -# } -# -# #[cgp_component { -# name: AuthTokenTypeComponent, -# provider: ProvideAuthTokenType, -# }] -# pub trait HasAuthTokenType { -# type AuthToken; -# } +# cgp_type!( Time ); +# cgp_type!( AuthToken ); # # #[cgp_component { # provider: AuthTokenValidator, @@ -219,21 +187,8 @@ For example, at a later time, we could replace the string error with a custom # # use cgp::prelude::*; # -# #[cgp_component { -# name: TimeTypeComponent, -# provider: ProvideTimeType, -# }] -# pub trait HasTimeType { -# type Time; -# } -# -# #[cgp_component { -# name: AuthTokenTypeComponent, -# provider: ProvideAuthTokenType, -# }] -# pub trait HasAuthTokenType { -# type AuthToken; -# } +# cgp_type!( Time ); +# cgp_type!( AuthToken ); # # #[cgp_component { # provider: AuthTokenValidator, @@ -360,21 +315,8 @@ of `From` to raise a source error like `&'static str`: # # use cgp::prelude::*; # -# #[cgp_component { -# name: TimeTypeComponent, -# provider: ProvideTimeType, -# }] -# pub trait HasTimeType { -# type Time; -# } -# -# #[cgp_component { -# name: AuthTokenTypeComponent, -# provider: ProvideAuthTokenType, -# }] -# pub trait HasAuthTokenType { -# type AuthToken; -# } +# cgp_type!( Time ); +# cgp_type!( AuthToken ); # # #[cgp_component { # provider: AuthTokenValidator, @@ -445,9 +387,9 @@ as follows: # use cgp::core::error::{ErrorRaiser, HasErrorType}; -pub struct RaiseIntoAnyhow; +pub struct RaiseAnyhowError; -impl ErrorRaiser for RaiseIntoAnyhow +impl ErrorRaiser for RaiseAnyhowError where Context: HasErrorType, SourceError: core::error::Error + Send + Sync + 'static, @@ -458,7 +400,7 @@ where } ``` -We define a provider `RaiseIntoAnyhow`, which implements the provider trait +We define a provider `RaiseAnyhowError`, which implements the provider trait `ErrorRaiser` with a generic context `Context` and a generic source error `SourceError`. Using impl-side dependencies, we also include an additional constraint that the implementation is only valid if `Context` implements `HasErrorType`, @@ -516,9 +458,9 @@ use core::fmt::Debug; use anyhow::anyhow; use cgp::core::error::{ErrorRaiser, HasErrorType}; -pub struct DebugAsAnyhow; +pub struct DebugAnyhowError; -impl ErrorRaiser for DebugAsAnyhow +impl ErrorRaiser for DebugAnyhowError where Context: HasErrorType, SourceError: Debug, @@ -529,13 +471,13 @@ where } ``` -The provider `DebugAsAnyhow` can raise any source error `SourceError` into `anyhow::Error`, +The provider `DebugAnyhowError` can raise any source error `SourceError` into `anyhow::Error`, given that `SourceError` implements `Debug`. To implement the `raise_error` method, we simply use the `anyhow!` macro, and format the source error using `Debug`. -With a context-generic error raiser like `DebugAsAnyhow`, a concrete context +With a context-generic error raiser like `DebugAnyhowError`, a concrete context can now use a provider like `ValidateTokenIsNotExpired`, which can use -`DebugAsAnyhow` to raise source errors that only implement `Debug`, such as +`DebugAnyhowError` to raise source errors that only implement `Debug`, such as `&'static str` and `ErrAuthTokenHasExpired`. ## Putting It Altogether @@ -545,6 +487,7 @@ from the previous chapter, and make it generic over the error type: ```rust # extern crate cgp; +# extern crate cgp_error_anyhow; # extern crate anyhow; # extern crate datetime; # @@ -552,21 +495,8 @@ from the previous chapter, and make it generic over the error type: pub mod traits { use cgp::prelude::*; - #[cgp_component { - name: TimeTypeComponent, - provider: ProvideTimeType, - }] - pub trait HasTimeType { - type Time; - } - - #[cgp_component { - name: AuthTokenTypeComponent, - provider: ProvideAuthTokenType, - }] - pub trait HasAuthTokenType { - type AuthToken; - } + cgp_type!( Time ); + cgp_type!( AuthToken ); #[cgp_component { provider: AuthTokenValidator, @@ -643,30 +573,6 @@ pub mod impls { Ok(LocalDateTime::now()) } } - - pub struct UseStringAuthToken; - - impl ProvideAuthTokenType for UseStringAuthToken { - type AuthToken = String; - } - - pub struct UseAnyhowError; - - impl ProvideErrorType for UseAnyhowError { - type Error = anyhow::Error; - } - - pub struct DebugAsAnyhow; - - impl ErrorRaiser for DebugAsAnyhow - where - Context: HasErrorType, - SourceError: Debug, - { - fn raise_error(e: SourceError) -> anyhow::Error { - anyhow!("{e:?}") - } - } } pub mod contexts { @@ -675,6 +581,7 @@ pub mod contexts { use anyhow::anyhow; use cgp::core::error::{ErrorRaiserComponent, ErrorTypeComponent}; use cgp::prelude::*; + use cgp_error_anyhow::{UseAnyhowError, DebugAnyhowError}; use datetime::LocalDateTime; use super::impls::*; @@ -693,12 +600,12 @@ pub mod contexts { delegate_components! { MockAppComponents { ErrorTypeComponent: UseAnyhowError, - ErrorRaiserComponent: DebugAsAnyhow, + ErrorRaiserComponent: DebugAnyhowError, [ TimeTypeComponent, CurrentTimeGetterComponent, ]: UseLocalDateTime, - AuthTokenTypeComponent: UseStringAuthToken, + AuthTokenTypeComponent: UseType, AuthTokenValidatorComponent: ValidateTokenIsNotExpired, } } @@ -730,7 +637,7 @@ only implementing `Debug`. We also define the provider `UseAnyhowError`, which implements `ProvideErrorType` by setting `Error` to `anyhow::Error`. Inside the component wiring for `MockAppComponents`, we wire up `ErrorTypeComponent` -with `UseAnyhowError`, and `ErrorRaiserComponent` with `DebugAsAnyhow`. +with `UseAnyhowError`, and `ErrorRaiserComponent` with `DebugAnyhowError`. Inside the context-specific implementation `AuthTokenExpiryFetcher`, we can use `anyhow::Error` directly, since Rust already knows that the type of `MockApp::Error` is `anyhow::Error`. diff --git a/src/lib.rs b/src/lib.rs index e69de29..8b13789 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -0,0 +1 @@ + From ea0465f9fab895d43706d1144f6eb1a82bdf0cf3 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Wed, 8 Jan 2025 20:03:22 +0000 Subject: [PATCH 2/4] Add section for cgp-error-anyhow --- content/error-handling.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/content/error-handling.md b/content/error-handling.md index ea64246..8e74573 100644 --- a/content/error-handling.md +++ b/content/error-handling.md @@ -480,9 +480,17 @@ can now use a provider like `ValidateTokenIsNotExpired`, which can use `DebugAnyhowError` to raise source errors that only implement `Debug`, such as `&'static str` and `ErrAuthTokenHasExpired`. +## The `cgp-error-anyhow` Crate + +The CGP project offers the [`cgp-error-anyhow`](https://docs.rs/cgp-error-anyhow) crate, which includes the anyhow-specific providers that we have discussed in this chapter. The constructs are not included as part of the core `cgp` crate, as we don't want to include `anyhow` as part of the crate dependencies. + +Similarly, there are other CGP error crates that use other error libraries as the error type. The [`cgp-error-eyre`](https://docs.rs/cgp-error-eyre) crate can work with `eyre::Error`, and the [`cgp-error-std`](https://docs.rs/cgp-error-std) crate can work with `Box`. + +As we can see in this chapter, CGP makes it very easy for entire projects to switch between error handling implementations, without being tightly coupled with a specific error type. Supposed that we want to run the application in a resource-constrained environment, we can swap the use of `cgp-error-anyhow` with `cgp-error-std` inside the component wiring, then the application would now make use of the simpler `Box` type to handle errors. + ## Putting It Altogether -With the use of `HasErrorType` and `CanRaiseError`, we can now refactor the full example +With the use of `HasErrorType`, `CanRaiseError`, and `cgp-error-anyhow`, we can now refactor the full example from the previous chapter, and make it generic over the error type: ```rust From 52991f32f9040115d4547ab88dfbc1acda2cbb9d Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Wed, 8 Jan 2025 20:38:07 +0000 Subject: [PATCH 3/4] AI-revise chapter --- content/error-handling.md | 229 ++++++++------------------------------ 1 file changed, 46 insertions(+), 183 deletions(-) diff --git a/content/error-handling.md b/content/error-handling.md index 8e74573..0e15708 100644 --- a/content/error-handling.md +++ b/content/error-handling.md @@ -1,42 +1,16 @@ # Error Handling -Rust provides a relatively new way of handling errors, with the use of `Result` type -to represent explicit errors. Compared to the practice of implicit exceptions in other -mainstream languages, the explicit `Result` type provides many advantages, such as -making it clear when and what kind of errors can occur when calling a function. -However, until now there is not yet a clear consensus of which _error type_ should -be used within a `Result`. - -The reason why choosing an error type is complicated is often due to different -applications having different concerns: Should the error capture stack traces? -Can the error be used in no_std environment? How should the error message be -displayed? Should the error contain _structured metadata_ that can be introspected -or logged differently? How should one differentiate different errors to decide -whether to retry an operation? How to compose or _flatten_ error sources that -come from using different libraries? etc. - -Due to the complex cross-cutting concerns, there are never-ending discussions -across the Rust communities on the quest to find a perfect error type that -can be used to solve _all_ error handling problems. At the moment, the -Rust ecosystem leans toward using error libraries such as -[`anyhow`](https://docs.rs/anyhow) to store error values using some -form of _dynamic typing_. However, these approaches give up some of the -advantages provided by static types, such as the ability to statically -know whether a function would never raise certain errors. - -CGP offers us an alternative approach towards error handling, which is -to use _abstract_ error types in `Result`, together with a context-generic -way of _raising errors_ without access to the concrete type. -In this chapter, we will walk through this new approach of error handling, -and look at how it allows error handling to be easily customized depending -on the exact needs of an application. +Rust introduces a modern approach to error handling through the use of the `Result` type, which explicitly represents errors. Unlike implicit exceptions commonly used in other mainstream languages, the `Result` type offers several advantages. It clearly indicates when errors may occur and specifies the type of errors that might be encountered when calling a function. However, the Rust community has yet to reach a consensus on the ideal _error type_ to use within a `Result`. + +Choosing an appropriate error type is challenging because different applications have distinct requirements. For instance, should the error include stack traces? Can it be compatible with no_std environments? How should the error message be presented? Should it include _structured metadata_ for introspection or specialized logging? How can different errors be distinguished to determine whether an operation should be retried? How can error sources from various libraries be composed or _flattened_ effectively? These and other concerns complicate the decision-making process. + +Because of these cross-cutting concerns, discussions in the Rust community about finding a universally optimal error type are never ending. Currently, the ecosystem tends to favor libraries like [`anyhow`](https://docs.rs/anyhow) that store error values using some form of _dynamic typing_. While convenient, these approaches sacrifice some benefits of static typing, such as the ability to determine at compile time whether a function cannot produce certain errors. + +CGP offers an alternative approach to error handling: using _abstract_ error types within `Result` alongside a context-generic mechanism for _raising errors_ without requiring a specific error type. In this chapter, we will explore this new approach, demonstrating how it allows error handling to be tailored to an application's precise needs. ## Abstract Error Type -In the previous chapter, we have learned about how to use associated types -together with CGP to define abstract types. -Similar to the abstract `Time` and `AuthToken` types, we can define an abstract -`Error` type as follows: +In the previous chapter, we explored how to use associated types with CGP to define abstract types. Similarly to abstract types like `Time` and `AuthToken`, we can define an abstract `Error` type as follows: ```rust # extern crate cgp; @@ -48,19 +22,11 @@ use cgp::prelude::*; cgp_type!( Error: Debug ); ``` -The trait `HasErrorType` is quite special, in the sense that it serves as a standard -type API for _all_ CGP components that make use of some form of abstract errors. -Because of this, it has a pretty minimal definition, having an associated type -`Error` with a default `Debug` constraint. We chose to require the `Debug` constraint -for abstract errors, because many Rust APIs such as `Result::unwrap` already -expect error types to implement `Debug`. +The `HasErrorType` trait is particularly significant because it serves as a standard type API for _all_ CGP components that involve abstract errors. Its definition is intentionally minimal, consisting of a single associated type, `Error`, constrained by `Debug` by default. This `Debug` constraint was chosen because many Rust APIs, such as `Result::unwrap`, rely on error types implementing `Debug`. -The use for `HasErrorType` is so common, that it is included as part of the `cgp` crate, -and is included in the prelude. So moving forward, we will import the `HasErrorType` trait -from `cgp`, instead of defining it locally. +Given its ubiquity, the `HasErrorType` trait is included as part of the `cgp` crate and is available in the prelude. Therefore, we will use the version provided by `cgp` rather than redefining it locally in subsequent examples. -Continuing from the example in the previous chapter, we can update authentication components -to use the abstract error type provided by `HasErrorType`: +Building on the example from the previous chapter, we can update authentication components to leverage the abstract error type defined by `HasErrorType`: ```rust # extern crate cgp; @@ -95,20 +61,13 @@ pub trait HasCurrentTime: HasTimeType + HasErrorType { } ``` -Each of the traits now include `HasErrorType` as a supertrait, and the methods now -return `Self::Error` instead of `anyhow::Error` in the returned `Result`. +In these examples, each trait now includes `HasErrorType` as a supertrait, and methods return `Self::Error` in the `Result` type instead of relying on a concrete type like `anyhow::Error`. This abstraction allows greater flexibility and customization, enabling components to adapt their error handling to the specific needs of different contexts. ## Raising Errors With `From` -Now that we have made use of abstract errors over concrete errors in our component interfaces, -a challenge that arise next is how can we raise abstract errors inside our context-generic -providers. -With CGP, we can make use of impl-side dependencies as usual, and include additional constraints -on the `Error` type, such as requiring it to implement `From` to convert a source error into -an abstract error value. +After adopting abstract errors in our component interfaces, the next challenge is handling these abstract errors in context-generic providers. With CGP, this is achieved by leveraging impl-side dependencies and adding constraints to the `Error` type, such as requiring it to implement `From`. This allows for the conversion of a source error into an abstract error value. -Using this technique, we can re-write `ValidateTokenIsNotExpired` to convert a source error -`&'static str` into `Context::Error`, when an auth token has expired: +For example, we can modify the `ValidateTokenIsNotExpired` provider to convert a source error, `&'static str`, into `Context::Error` when an authentication token has expired: ```rust # extern crate cgp; @@ -167,18 +126,9 @@ where } ``` -As we can see from the example, CGP makes it easy to make use of "stringy" error handling -inside context-generic providers, by offloading the task of converting from strings to the -actual error value to the concrete application. -Although the use of strings as error is not exactly a good practice, it can be very helpful -during rapid prototyping phase, when we don't yet care about how exactly we want to handle -various errors. +This example demonstrates how CGP simplifies "stringy" error handling in context-generic providers by delegating the conversion from strings to concrete error values to the application. While using string errors is generally not a best practice, it is useful during the prototyping phase when precise error handling strategies are not yet established. -With CGP, we want to enable an iterative approach, where developers can make the choise to -use stringly errors in the early stage, and then gradually transition toward more structured -error handling at later stages of development. -For example, at a later time, we could replace the string error with a custom -`ErrAuthTokenHasExpired` as follows: +CGP encourages an iterative approach to error handling. Developers can begin with string errors for rapid prototyping and transition to structured error handling as the application matures. For example, we can replace the string error with a custom error type like `ErrAuthTokenHasExpired`: ```rust # extern crate cgp; @@ -248,42 +198,17 @@ impl Display for ErrAuthTokenHasExpired { } ``` -Compared to before, we defined an `ErrAuthTokenHasExpired` type to represent the -error that happens when an auth token has expired. -Inside `AuthTokenValidator`, we now require `Context::Error` to implement `From` -to convert an expired token error into the abstract error. -The type `ErrAuthTokenHasExpired` implements both `Debug` and `Display`, so that the application -may use them when converting into `Context::Error`. - -CGP makes it easy to define provider-specific error types such as `ErrAuthTokenHasExpired`, -without requiring the provider to worry about how to embed that error within the application -error as a whole. -With impl-side dependencies, an extra constraint like `Context::Error: From` -would only be applicable if the application choose to use the specific provider. -This also means that if an application chose a provider other than `ValidateTokenIsNotExpired` -to implement `AuthTokenValidator`, then it would not need to handle the error `ErrAuthTokenHasExpired`. +In this example, we introduced the `ErrAuthTokenHasExpired` type to represent the specific error of an expired authentication token. The `AuthTokenValidator` implementation requires `Context::Error` to implement `From` for conversion to the abstract error type. Additionally, `ErrAuthTokenHasExpired` implements both `Debug` and `Display`, allowing applications to present and log the error meaningfully. + +CGP facilitates defining provider-specific error types like `ErrAuthTokenHasExpired` without burdening the provider with embedding these errors into the application's overall error handling strategy. With impl-side dependencies, constraints like `Context::Error: From` apply only when the application uses a specific provider. If an application employs a different provider to implement `AuthTokenValidator`, it does not need to handle the `ErrAuthTokenHasExpired` error. ## Raising Errors using `CanRaiseError` -In the previous section, we used the `From` constraint in the provider implementation of -`ValidateTokenIsNotExpired` to raise either `&'static str` or `ErrAuthTokenHasExpired`. -Although this approach looks elegant, we would quickly realized that this approach -would _not_ work with popular error types such as `anyhow::Error`. -This is because `anyhow::Error` only provide a blanket `From` instance for types -that `core::error::Error + Send + Sync + 'static`. - -This restriction is a common pain point when using error libraries like `anyhow`. -But the restriction is there because without CGP, a type like `anyhow::Error` -cannot provide other blanket implementations for `From` as it would cause overlap. -The use of `From` also causes leaky abstraction, as custom error types like -`ErrAuthTokenHasExpired` are forced to anticipate such use and implement the -common constraints like `core::error::Error`. -Furthermore, the ownership rules also make it impossible to support custom `From` -implementations for non-owned types, such as `String` and `&str`. - -For these reasons, we don't actually encourage the use of `From` for conversion -into abstract errors. Instead, with CGP we prefer the use of a more flexible, -albeit more verbose approach, which is to use the `CanRaiseError` trait: +In the previous section, we used the `From` constraint in the `ValidateTokenIsNotExpired` provider to raise errors such as `&'static str` or `ErrAuthTokenHasExpired`. While this approach is elegant, we quickly realize it doesn't work with common error types like `anyhow::Error`. This is because `anyhow::Error` only provides a blanket From implementation only for types that implement `core::error::Error + Send + Sync + 'static`. + +This restriction is a common pain point when using error libraries like `anyhow`. The reason for this limitation is that without CGP, a type like `anyhow::Error` cannot provide multiple blanket `From` implementations without causing conflicts. As a result, using `From` can leak abstractions, forcing custom error types like `ErrAuthTokenHasExpired` to implement common traits like `core::error::Error`. Another challenge is that ownership rules prevent supporting custom `From` implementations for non-owned types like `String` and `&str`. + +To address these issues, we recommend using a more flexible — though slightly more verbose—approach with CGP: the `CanRaiseError` trait, rather than relying on `From` for error conversion. Here's how we define it: ```rust # extern crate cgp; @@ -298,17 +223,11 @@ pub trait CanRaiseError: HasErrorType { } ``` -The trait `CanRaiseError` contains a _generic parameter_ `SourceError` that represents a -source error type that we want to embed into the main abstract error, -`HasErrorType::Error`. -By having it as a generic parameter, it means that a context can raise multiple -source error types `SourceError` by converting it into `HasErrorType::Error`. +The `CanRaiseError` trait has a _generic parameter_ `SourceError`, representing the source error type that will be converted into the abstract error type `HasErrorType::Error`. By making it a generic parameter, this allows a context to raise multiple source error types and convert them into the abstract error. -Since raising errors is essential in almost all CGP code, the `CanRaiseError` -trait is also included as part of the prelude in `cgp`. +Since raising errors is common in most CGP code, the `CanRaiseError` trait is included in the CGP prelude, so we don’t need to define it manually. -We can now redefine `ValidateTokenIsNotExpired` to use `CanRaiseError` instead -of `From` to raise a source error like `&'static str`: +We can now update the `ValidateTokenIsNotExpired` provider to use `CanRaiseError` instead of `From` for error handling, raising a source error like `&'static str`: ```rust # extern crate cgp; @@ -366,20 +285,13 @@ where } ``` -In the new implementation, we replace the constraint `Context: HasErrorType` with -`Context: CanRaiseError<&'static str>`. Since `HasErrorType` is a super trait of -`CanRaiseError`, we only need to include `CanRaiseError` in the constraint to -automatically also include the `HasErrorType` constraint. -We also use the method `Context::raise_error` to raise the string -`"auth token has expired"` to become `Context::Error`. +In this updated implementation, we replace the `Context: HasErrorType` constraint with `Context: CanRaiseError<&'static str>`. Since `HasErrorType` is a supertrait of `CanRaiseError`, we only need to include `CanRaiseError` in the constraint to automatically include `HasErrorType`. We also use the `Context::raise_error` method to convert the string `"auth token has expired"` into `Context::Error`. + +This approach avoids the limitations of `From` and offers greater flexibility for error handling in CGP, especially when working with third-party error types like `anyhow::Error`. ## Context-Generic Error Raisers -By defining our own `CanRaiseError` trait using CGP, we get to overcome the various -limitations of `From`, and implement context-generic error raisers that are generic -over the source error. -For example, we can implement a context-generic error raiser for `anyhow::Error` -as follows: +By defining the `CanRaiseError` trait using CGP, we overcome the limitations of `From` and enable context-generic error raisers that work across various source error types. For instance, we can create a context-generic error raiser for `anyhow::Error` as follows: ```rust # extern crate cgp; @@ -400,22 +312,9 @@ where } ``` -We define a provider `RaiseAnyhowError`, which implements the provider trait -`ErrorRaiser` with a generic context `Context` and a generic source error `SourceError`. -Using impl-side dependencies, we also include an additional constraint that -the implementation is only valid if `Context` implements `HasErrorType`, -_and_ if `Context::Error` is `anyhow::Error`. -We also require a constraint for the source error `SourceError` to implement -`core::error::Error + Send + Sync + 'static`, which is required to use -the `From` instance of `anyhow::Error`. -Inside the method signature, we can replace the return value from `Context::Error` -to `anyhow::Error`, since we already required the two types to be equal. -Inside the method body, we simply call `e.into()` to convert the source -error `SourceError` using `anyhow::Error::From`, since the constraint for using -it is already satisfied. - -In fact, if our purpose is to use `From` to convert the errors, we can implement -a generalized provider that work with any instance of `From` as follows: +Here, `RaiseAnyhowError` is a provider that implements the `ErrorRaiser` trait with generic `Context` and `SourceError`. The implementation is valid only if the `Context` implements `HasErrorType` and implements `Context::Error` as `anyhow::Error`. Additionally, the `SourceError` must satisfy `core::error::Error + Send + Sync + 'static`, which is necessary for the `From` implementation provided by `anyhow::Error`. Inside the method body, the source error is converted into `anyhow::Error` using `e.into()` since the required constraints are already satisfied. + +For a more generalized approach, we can create a provider that works with _any_ error type supporting `From`: ```rust # extern crate cgp; @@ -435,19 +334,9 @@ where } ``` -The `RaiseFrom` provider can work with any `Context` that implements `HasErrorType`, -without further qualification of what the concrete type for `Context::Error` should be. -The only additional requirement is that `Context::Error` needs to implement `From`. -With that constraint in place, we can once again raise errors from any source error `SourceError` -to `Context::Error`, without coupling it explicitly in providers like -`ValidateTokenIsNotExpired`. - -It may seems redundant that we introduce the indirection of `CanRaiseError`, just to -use back `From` to convert errors in the end. But the main purpose for this redirection -is so that we can use something other than `From` to convert errors. -For example, we can define a context-generic provider for `anyhow::Error` that -raise errors using `Debug` instead of `From`: +This implementation requires the `Context` to implement `HasErrorType` and the `Context::Error` type to implement `From`. With these constraints in place, this provider allows errors to be raised from any source type to `Context::Error` using `From`, without requiring explicit coupling in providers like `ValidateTokenIsNotExpired`. +The introduction of `CanRaiseError` might seem redundant when it ultimately relies on `From` in some cases. However, the purpose of this indirection is to enable _alternative_ mechanisms for converting errors when `From` is insufficient or unavailable. For example, we can define an error raiser for `anyhow::Error` that uses the `Debug` trait instead of `From`: ```rust # extern crate cgp; @@ -471,22 +360,15 @@ where } ``` -The provider `DebugAnyhowError` can raise any source error `SourceError` into `anyhow::Error`, -given that `SourceError` implements `Debug`. To implement the `raise_error` method, we -simply use the `anyhow!` macro, and format the source error using `Debug`. - -With a context-generic error raiser like `DebugAnyhowError`, a concrete context -can now use a provider like `ValidateTokenIsNotExpired`, which can use -`DebugAnyhowError` to raise source errors that only implement `Debug`, such as -`&'static str` and `ErrAuthTokenHasExpired`. +In this implementation, the `DebugAnyhowError` provider raises any source error into an `anyhow::Error`, as long as the source error implements `Debug`. The `raise_error` method uses the `anyhow!` macro and formats the source error using the `Debug` trait. This approach allows a concrete context to use providers like `ValidateTokenIsNotExpired` while relying on `DebugAnyhowError` to raise source errors such as `&'static str` or `ErrAuthTokenHasExpired`, which only implement `Debug` or `Display`. ## The `cgp-error-anyhow` Crate -The CGP project offers the [`cgp-error-anyhow`](https://docs.rs/cgp-error-anyhow) crate, which includes the anyhow-specific providers that we have discussed in this chapter. The constructs are not included as part of the core `cgp` crate, as we don't want to include `anyhow` as part of the crate dependencies. +The CGP project provides the [`cgp-error-anyhow`](https://docs.rs/cgp-error-anyhow) crate, which includes the anyhow-specific providers discussed in this chapter. These constructs are offered as a separate crate rather than being part of the core `cgp` crate to avoid adding `anyhow` as a mandatory dependency. -Similarly, there are other CGP error crates that use other error libraries as the error type. The [`cgp-error-eyre`](https://docs.rs/cgp-error-eyre) crate can work with `eyre::Error`, and the [`cgp-error-std`](https://docs.rs/cgp-error-std) crate can work with `Box`. +In addition, CGP offers other error crates tailored to different error handling libraries. The [`cgp-error-eyre`](https://docs.rs/cgp-error-eyre) crate supports `eyre::Error`, while the [`cgp-error-std`](https://docs.rs/cgp-error-std) crate works with `Box`. -As we can see in this chapter, CGP makes it very easy for entire projects to switch between error handling implementations, without being tightly coupled with a specific error type. Supposed that we want to run the application in a resource-constrained environment, we can swap the use of `cgp-error-anyhow` with `cgp-error-std` inside the component wiring, then the application would now make use of the simpler `Box` type to handle errors. +As demonstrated in this chapter, CGP allows projects to easily switch between error handling implementations without being tightly coupled to a specific error type. For instance, if the application needs to run in a resource-constrained environment, replacing `cgp-error-anyhow` with `cgp-error-std` in the component wiring enables the application to use the simpler `Box` type for error handling. ## Putting It Altogether @@ -639,31 +521,12 @@ pub mod contexts { # } ``` -In the new code, we refactored `ValidateTokenIsNotExpired` to make use of -`CanRaiseError`, with `ErrAuthTokenHasExpired` -only implementing `Debug`. -We also define the provider `UseAnyhowError`, which implements `ProvideErrorType` -by setting `Error` to `anyhow::Error`. -Inside the component wiring for `MockAppComponents`, we wire up `ErrorTypeComponent` -with `UseAnyhowError`, and `ErrorRaiserComponent` with `DebugAnyhowError`. -Inside the context-specific implementation `AuthTokenExpiryFetcher`, -we can use `anyhow::Error` directly, since Rust already knows that the type of -`MockApp::Error` is `anyhow::Error`. +In the updated code, we refactored `ValidateTokenIsNotExpired` to use `CanRaiseError`, with `ErrAuthTokenHasExpired` implementing only `Debug`. Additionally, we use the provider `UseAnyhowError` from `cgp-error-anyhow`, which implements `ProvideErrorType` by setting `Error` to `anyhow::Error`. + +In the component wiring for `MockAppComponents`, we wire up `ErrorTypeComponent` with `UseAnyhowError` and `ErrorRaiserComponent` with `DebugAnyhowError`. In the context-specific implementation `AuthTokenExpiryFetcher`, we can now use `anyhow::Error` directly, since Rust already knows that `MockApp::Error` is the same type as `anyhow::Error`. ## Conclusion -In this chapter, we have gone through a high level overview of how the approach -for error handling in CGP is very different from how error handling is typically -done in Rust. By making use of abstract error types with `HasErrorType`, we are -able to implement providers that are generic over the concerete error type -used by an application. By raising error sources using `CanRaiseError`, we can -implement context-generic error raisers that workaround the limitations of -non-overlapping impls, and work with source errors that only implement traits -like `Debug`. - -Nevertheless, error handling is a complex topic of its own, and the CGP abstractions -like `HasErrorType` and `CanRaiseError` can only serve as the foundation to tackle -this complex problem. -There are a few more details related to error handling, which we will cover -in the next chapters, before we can be ready to handle errors in real world -applications. \ No newline at end of file +In this chapter, we provided a high-level overview of how error handling in CGP differs significantly from traditional error handling done in Rust. By utilizing abstract error types with `HasErrorType`, we can create providers that are generic over the concrete error type used by an application. The `CanRaiseError` trait allows us to implement context-generic error raisers, overcoming the limitations of non-overlapping implementations and enabling us to work with source errors that only implement traits like `Debug`. + +However, error handling is a complex subject, and CGP abstractions such as `HasErrorType` and `CanRaiseError` are just the foundation for addressing this complexity. There are additional details related to error handling that we will explore in the upcoming chapters, preparing us to handle errors effectively in real-world applications. From e2f49d842b2172b79f5d8b499f8cbe540e17f458 Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Wed, 8 Jan 2025 20:56:43 +0000 Subject: [PATCH 4/4] AI-revise delegated error raisers --- content/delegated-error-raiser.md | 188 +++++++----------------------- 1 file changed, 43 insertions(+), 145 deletions(-) diff --git a/content/delegated-error-raiser.md b/content/delegated-error-raiser.md index 432ea29..753e022 100644 --- a/content/delegated-error-raiser.md +++ b/content/delegated-error-raiser.md @@ -1,22 +1,14 @@ # Delegated Error Raisers -In the previous chapter, we have defined context-generic error raisers like `RaiseFrom` -and `DebugAsAnyhow`, which can be use to raise any source error that satisfy certain -constraints. -However, in the main wiring for `MockAppComponents`, we could only choose a specific -provider for `ErrorRaiserComponent`. -But with complex applications, we may want to raise different source errors differently, -depending on what the source error is. -For example, we may want to use `RaiseFrom` when there is a `From` instance, and -`DebugAsAnyhow` for the remaining cases when the source error implements `Debug`. - -In this chapter, we will cover the `UseDelegate` pattern, which offers a declarative -way to handle errors differently depending on the source error type. +In the previous chapter, we defined context-generic error raisers such as `RaiseFrom` and `DebugAnyhowError`, which can be used to raise any source error that satisfies certain constraints. However, in the main wiring for `MockAppComponents`, we could only select a specific provider for the `ErrorRaiserComponent`. + +In more complex applications, we might want to handle different source errors in different ways, depending on the type of the source error. For example, we might use `RaiseFrom` when a `From` instance is available, and default to `DebugAnyhowError` for cases where the source error implements `Debug`. + +In this chapter, we will introduce the _`UseDelegate`_ pattern, which provides a declarative approach to handle errors differently based on the source error type. ## Ad Hoc Error Raisers -One way that we can handle source errors differently is by defining an error raiser -provider that has explicit implementation for each source error, such as follows: +One way to handle source errors differently is by defining an error raiser provider with explicit implementations for each source error. For example: ```rust # extern crate cgp; @@ -98,37 +90,15 @@ where } ``` -In the above example, we define a provider `MyErrorRaiser` that have explicit -`ErrorRaiser` implementation for a limited list of source error types, with -the assumption that the abstract `Context::Error` is instantiated to -`anyhow::Error`. - -With explicit implementations, `MyErrorRaiser` is able to implement different -strategy to handle different source error. -When raising a source error `anyhow::Error`, we simply return `e` as `Context::Error` -is also `anyhow::Error`. -When raising `Infallible`, we can unconditionally handle the error by matching with -empty case. -When raising `std::io::Error` and `ParseIntError`, we can just use the `From` instance, -since they satisfy the constraint `core::error::Error + Send + Sync + 'static`. -When raising `ErrAuthTokenHasExpired`, we format the error using `anyhow!` with -the `Debug` instance. -When raising `String` and `&'a str`, we format the error using `anyhow!` with -the `Display` instance. - -The approach of defining explicit `ErrorRaiser` implementations gives us a lot of -flexibility, but at the cost of requiring a lot of non-reusable boilerplate. -Given that we have previously defined various generic error raisers, it would -be good if there is a way to dispatch the error handling to different error -raiser, depending on the source error type. +In this example, we define the provider `MyErrorRaiser` with explicit `ErrorRaiser` implementations for a set of source error types, assuming that the abstract `Context::Error` is `anyhow::Error`. + +With explicit implementations, `MyErrorRaiser` handles different source errors in various ways. When raising a source error of type `anyhow::Error`, we simply return `e` because `Context::Error` is also `anyhow::Error`. For `Infallible`, we handle the error by matching the empty case. For `std::io::Error` and `ParseIntError`, we rely on the `From` instance, as they satisfy the constraint `core::error::Error + Send + Sync + 'static`. When raising `ErrAuthTokenHasExpired`, we use the `anyhow!` macro to format the error with the `Debug` instance. For `String` and `&'a str`, we use `anyhow!` to format the error with the `Display` instance. + +While defining explicit `ErrorRaiser` implementations provides a high degree of flexibility, it also requires a significant amount of repetitive boilerplate. Since we’ve already defined various generic error raisers, it would be beneficial to find a way to _delegate_ error handling to different error raisers based on the source error type. ## `UseDelegate` Pattern -If we look closely to the patterns of implementing custom error raisers, we would -notice that it looks similar to the [provider delegation](./provider-delegation.md) -pattern that we have went through in the earlier chapter. -In fact, with a little bit of indirection, we can reuse `DelegateComponent` to -also delegate the handling of source errors for us: +When examining the patterns for implementing custom error raisers, we notice similarities to the [provider delegation](./provider-delegation.md) pattern we covered in an earlier chapter. In fact, with a bit of indirection, we can reuse `DelegateComponent` to delegate the handling of source errors: ```rust # extern crate cgp; @@ -153,25 +123,13 @@ where } ``` -We will walk through the code above slowly to uncover what it entails. First, we define a -`UseDelegate` struct with a `Components` phantom parameter. The type `UseDelegate` is used -as a _marker type_ for implementing trait-specific component delegation pattern. -For this case, we implement `ErrorRaiser` for `UseDelegate`, so that it can be used -as a context-generic provider for `ErrorRaiser` under specific conditions. +Let's walk through the code step by step. First, we define the `UseDelegate` struct with a phantom `Components` parameter. `UseDelegate` serves as a _marker_ type for implementing the trait-specific component delegation pattern. Here, we implement `ErrorRaiser` for `UseDelegate`, allowing it to act as a context-generic provider for `ErrorRaiser` under specific conditions. -Inside implementation, we specify that for any context `Context`, source error `SourceError`, -and error raiser components `Components`, `UseDelegate` implements -`ErrorRaiser` if `Components` implements `DelegateComponent`. -Additionally, the delegate `Components::Delegate` is also expected to implement -`ErorrRaiser`. Inside the `raise_error` method, we simply delegate the -implementation to `Components::Delegate::raise_error`. +Within the implementation, we specify that for any context `Context`, source error `SourceError`, and error raiser provider `Components`, `UseDelegate` implements `ErrorRaiser` if `Components` implements `DelegateComponent`. Additionally, the delegate `Components::Delegate` must also implement `ErrorRaiser`. Inside the `raise_error` method, we delegate the implementation to `Components::Delegate::raise_error`. -To explain it in simpler terms, `UseDelegate` implements `ErrorRaiser` -if there is a delegated provider `ErrorRaiser` that is delegated from `Components` -via `SourceError`. +In simpler terms, `UseDelegate` implements `ErrorRaiser` if there is a delegated provider `ErrorRaiser` from `Components` via `SourceError`. -We could better understand what this entails with a concrete example. Using `UseDelegate`, we can for -example declaratively dispatch errors such as follows: +We can better understand this by looking at a concrete example. Using `UseDelegate`, we can declaratively dispatch errors as follows: ```rust # extern crate cgp; @@ -189,9 +147,9 @@ example declaratively dispatch errors such as follows: # #[derive(Debug)] # pub struct ErrAuthTokenHasExpired; # -# pub struct DebugAsAnyhow; +# pub struct DebugAnyhowError; # -# impl ErrorRaiser for DebugAsAnyhow +# impl ErrorRaiser for DebugAnyhowError # where # Context: HasErrorType, # E: Debug, @@ -225,43 +183,24 @@ delegate_components! { [ ErrAuthTokenHasExpired, ]: - DebugAsAnyhow, + DebugAnyhowError, } } pub type MyErrorRaiser = UseDelegate; ``` -We first define a `MyErrorRaiserComponents` type, and use `delegate_components!` on it -to map the source error type to the error raiser provider we want to use. -We then redefine `MyErrorRaiser` to be just `UseDelegate`. -With that, we are able to implement `ErrorRaiser` for the source errors -`std::io::Error`, `ParseIntError`, and `ErrAuthTokenHasExpired`. - -Using the example, we can also trace back the `ErrorRaiser` implementation for `UseDelegate`, -and see how the handling of a source error like `std::io::Error` is wired. -First of all, `UseDelegate` implements `ErrorRaiser`, given that -`MyErrorRaiserComponents` implements `DelegateComponent`. -Following that, we can see that the `Delegate` is `RaiseFrom`, and for the case -when `Context::Error` is `anyhow::Error`, there is a `From` instance from `std::io::Error` -to `anyhow::Error`. Therefore, the chain of dependencies are satisfied, and so -the `ErrorRaiser` is implemented. - -As we can see from above, the CGP constructs `DelegateComponent` and `delegate_components!` -are not only useful for wiring up CGP providers, but we can also use the same pattern -for dispatching providers based on the generic parameters of specific traits. -In fact, we will see the same pattern being used again in many other domains. - -For that reason, the `UseDelegate` type is included as part of the `cgp` crate, together -with the `ErrorRaiser` implementation for it. This is so that readers can quickly identify -that the delegation is used, every time they see that a trait is implemented for `UseDelegate`. +In this example, we first define `MyErrorRaiserComponents` and use `delegate_components!` to map source error types to the error raiser providers we wish to use. Then, we redefine `MyErrorRaiser` to be `UseDelegate`. This allows us to implement `ErrorRaiser` for source errors such as `std::io::Error`, `ParseIntError`, and `ErrAuthTokenHasExpired`. + +We can also trace the `ErrorRaiser` implementation for `UseDelegate` and see how errors like `std::io::Error` are handled. First, `UseDelegate` implements `ErrorRaiser` because `MyErrorRaiserComponents` implements `DelegateComponent`. From there, we observe that the delegate is `RaiseFrom`, and for the case where `Context::Error` is `anyhow::Error`, a `From` instance exists for converting `std::io::Error` into `anyhow::Error`. Thus, the chain of dependencies is satisfied, and `ErrorRaiser` is implemented successfully. + +As seen above, the `DelegateComponent` and `delegate_components!` constructs are not only useful for wiring up CGP providers but can also be used to dispatch providers based on the generic parameters of specific traits. In fact, we will see the same pattern applied in other contexts throughout CGP. + +For this reason, the `UseDelegate` type is included in the `cgp` crate, along with the `ErrorRaiser` implementation, so that readers can easily identify when delegation is being used every time they encounter a trait implemented for `UseDelegate`. ## Forwarding Error Raiser -Aside form the delegation pattern, it can also be useful to implement generic error raisers -that perform some transformation of the source error, and then forward the handling to another -error raiser. For example, when implementing a generic error raiser that uses `Debug` on -the source error, we could first format it and then raise it as a string as follows: +In addition to the delegation pattern, it can be useful to implement generic error raisers that perform a transformation on the source error and then forward the handling to another error raiser. For instance, when implementing a generic error raiser that formats the source error using `Debug`, we could first format it as a string and then forward the handling as follows: ```rust # extern crate cgp; @@ -282,27 +221,15 @@ where } ``` -In the example above, we define a generic error raiser `DebugError`, which implements -`ErrorRaiser` for any `SourceError` that implements `Debug`. -Additionally, we also require that `Context` implements `CanRaiseError`. -Inside the implementation of `raise_error`, we simply format the source error as -a string, and then call `Context::raise_error` again on the formatted string. +In the example above, we define a generic error raiser `DebugError` that implements `ErrorRaiser` for any `SourceError` that implements `Debug`. Additionally, we require that `Context` also implements `CanRaiseError`. Inside the implementation of `raise_error`, we format the source error as a string and then invoke `Context::raise_error` with the formatted string. -A forwarding error raiser like `DebugError` is inteded to be used together with -`UseDelegate`, so that the `ErrorRaiser` implementation of `String` is expected -to be handled by a concrete error raiser. Otherwise, an incorrect wiring may -result in a stack overflow, if `DebugError` ended up calling itself again -to handle the error raising of `String`. +A forwarding error raiser like `DebugError` is designed to be used with `UseDelegate`, ensuring that the `ErrorRaiser` implementation for `String` is handled by a separate error raiser. Without this, an incorrect wiring could result in a stack overflow if `DebugError` were to call itself recursively when handling the `String` error. -Nevertheless, the main advantage for this definition is that it is also generic -over the abstract `Context::Error` type. So when used carefully, we can keep a lot -of error handling code fully context-generic this way. +The key advantage of this approach is that it remains generic over the abstract `Context::Error` type. When used correctly, this allows for a large portion of error handling to remain fully context-generic, promoting flexibility and reusability. ## Full Example -Now that we have learned about how to use `UseDelegate`, we can rewrite the naive -error raiser that we defined in the beginning of this chapter, and use `delegate_components!` -to simplify our error handling. +Now that we have learned how to use `UseDelegate`, we can rewrite the naive error raiser from the beginning of this chapter and use `delegate_components!` to simplify our error handling. ```rust # extern crate cgp; @@ -372,9 +299,9 @@ pub mod impls { type Error = anyhow::Error; } - pub struct DisplayAsAnyhow; + pub struct DisplayAnyhowError; - impl ErrorRaiser for DisplayAsAnyhow + impl ErrorRaiser for DisplayAnyhowError where Context: HasErrorType, SourceError: Display, @@ -429,7 +356,7 @@ pub mod contexts { String, <'a> &'a str, ]: - DisplayAsAnyhow, + DisplayAnyhowError, } } @@ -449,47 +376,18 @@ pub mod contexts { # } ``` -In the first part of the above example, we define various context-generic error raisers -that are not only useful for our specific application, but can also be reused later for -other applications. We have `ReturnError` which returns the source error as is, -`RaiseFrom` to use `From` to convert the source error, `RaiseInfallible` to unconditionally -match `Infallible`, and `DebugError` to format and re-raise the error as string. -We also define `UseAnyhow` to implement `ProvideErrorType`, and `DisplayAsAnyhow` -to convert any `SourceError` implementing `Display` to `anyhow::Error`. - -In the second part of the example, we define a dummy context `MyApp` with the only purpose -is to show how it can handle various source errors. We define `MyErrorRaiserComponents`, -and use `delegate_components!` to map various source error types to use the error -raiser provider that we want to designate. We then use `UseDelegate` -as the provider for `ErrorRaiserComponent`. Finally, we define a check trait -`CanRaiseMyAppErrors`, and verify that the wiring for all error raisers are working correctly. +In the first part of the example, we define various context-generic error raisers that are useful not only for our specific application but can also be reused later for other applications. We have `ReturnError`, which simply returns the source error as-is, `RaiseFrom` for converting the source error using `From`, `RaiseInfallible` for handling `Infallible` errors, and `DebugError` for formatting and re-raising the error as a string. We also define `UseAnyhow` to implement `ProvideErrorType`, and `DisplayAnyhowError` to convert any `SourceError` implementing `Display` into `anyhow::Error`. + +In the second part of the example, we define a dummy context, `MyApp`, to illustrate how it can handle various source errors. We define `MyErrorRaiserComponents` and use `delegate_components!` to map various source error types to the corresponding error raiser providers. We then use `UseDelegate` as the provider for `ErrorRaiserComponent`. Finally, we define the trait `CanRaiseMyAppErrors` to verify that all the error raisers are wired correctly. ## Wiring Checks -As we can see from the example, the use of `UseDelegate` with `ErrorRaiser` effectively -serves as something similar to a top-level error handler for an application. -The main difference is that this "handling" of error is done entirely at compile-time. -This allows us to easily customize how exactly we want to handle each source error in -our application, and not pay for any performance overhead to achieve this level of -customization. - -One thing to note however, is that the wiring for delegated error raisers is done _lazily_, -similar to how the wiring is done for CGP providers. As a result, we may incorrectly wire -a source error type to use an error raiser provider with unsatisfied constraints, and only -get a compile-time error later on when the error raiser is used in another provider. - -Because of this, having misconfigured wiring of error raisers can be a common source of CGP -errors, especially for beginners. -We would encourage readers to revisit the chapter on [debugging techniques](./debugging-techniques.md) -and use the check traits to ensure that the handling of all source errors are wired correctly. -It would often helps to use the forked Rust compiler, to show the unsatisfied constraints -that arise from incomplete error raiser implementations. +As seen in the example, the use of `UseDelegate` with `ErrorRaiser` acts as a form of top-level error handler for an application. The main difference is that the "handling" of errors is done entirely at compile-time, enabling us to customize how each source error is handled without incurring any runtime performance overhead. + +However, it's important to note that the wiring for delegated error raisers is done _lazily_, similar to how CGP provider wiring works. This means that an error could be wired incorrectly, with constraints that are not satisfied, and the issue will only manifest as a compile-time error when the error raiser is used in another provider. + +Misconfigured wiring of error raisers can often lead to common CGP errors, especially for beginners. We encourage readers to refer back to the chapter on [debugging techniques](./debugging-techniques.md) and utilize check traits to ensure all source errors are wired correctly. It's also helpful to use a forked Rust compiler to display unsatisfied constraints arising from incomplete error raiser implementations. ## Conclusion -In this chapter, we have learned about using the `UseDelegate` pattern to declaratively -handle the error raisers in different ways. -As we will see in future chapters, the `UseDelegate` can also be applied to many other -problem domains in CGP. -The pattern is also essential for us to apply more advanced error handling techniques, -which we will cover in the next chapter. \ No newline at end of file +In this chapter, we explored the `UseDelegate` pattern and how it allows us to declaratively handle error raisers in various ways. This pattern simplifies error handling and can be extended to other problem domains within CGP, as we'll see in future chapters. Additionally, the `UseDelegate` pattern serves as a foundation for more advanced error handling techniques, which will be covered in the next chapter.