-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Problem Statement
CGP currently provide a strong foundation for modular error handling through components such as HasErrorType and CanRaiseError. However, using these traits alone are insufficient to cover all advanced error handling cases, especially for production applications.
The main problem is that it is tedious to define custom error types to be "thrown" by CanRaiseError. For example, suppose that we want to raise a HTTP error with details that contain abstract types. Currently a fully generalized error handling would require something like follows:
pub struct HttpClientError<'a, Context>
where
Context: HasUrlType + HasStatusCodeType,
{
pub context: &'a Context,
pub url: &'a Context::Url,
pub status_code: &'a Context::StatusCode,
}
impl<'a, Context> Debug for HttpClientError<'a, Context>
where
Context: HasUrlType + HasStatusCodeType,
Context::Url: Display,
Context::StatusCode: Display,
{ ... }
#[cgp_impl]
impl<Context> HttpRequestProvider for Context
where
Context: CanRaiseError<HttpClientError<'a, Context>> + ...,
{ ... }As we can see, there are way too much boilerplate needed, especially if the error detail contains abstract types.
As a result, if we want to convince developers to use the modular handling patterns by CGP, we need further abstraction to simplify error handling in CGP.
The #[cgp_auto_error] Macro
We will introduce the #[cgp_auto_error] macro, which follows similar spirit as #[cgp_auto_getter], and provide higher-level interfaces for handling errors in CGP code.
With it, the error handling code above will be simplified to something like:
#[cgp_auto_error]
pub trait CanRaiseHttpClientError: HasUrlType + HasStatusCodeType + HasErrorType {
#[error("HTTP request to URL {} returned error status code: {}", self.url, self.status_code)]
fn raise_http_client_error(
&self,
#[use(Display)]
url: &Self::Url,
#[use(Display)]
status_code: &Self::StatusCode,
) -> Self::Error;
#[error("HTTP request to URL {} encountered network error", self.url)]
fn raise_network_error(
&self,
#[use(Display)]
url: &Self::Url,
#[source_error]
error: std::io::Error,
) -> Self::Error;
}
#[cgp_impl]
impl<Context> HttpRequestProvider for Context
where
Context: CanRaiseHttpClientError + ...,
{ ... }Behind the scene, the #[cgp_auto_error] trait generates the error structs or enums similar to the original code, and generates blanket implementation for the trait that uses CanRaiseError to raise the error behind the scene.
With #[cgp_auto_error], we can hopefully simplify the error handling ergonomics in CGP, and make it easier for users to define custom errors to be handled.
Named Error
The #[cgp_auto_error] trait is used for raising errors, but we also need to improve the ergonomics of handling errors by the concrete context inside the wiring of error handlers through UseDelegate. The main problem is that custom error structs will contain where clauses, which makes it very difficult to refer to the struct type directly.
To work around it, we will introduce a Named trait as follows:
pub trait Named<'a, Context> {
type Type;
}The main idea is that we will defunctionalize the struct with a name, so that we can dispatch more easily using the name type. For example, the HttpClientError would be given a name like:
pub struct HttpClientErrorName;
impl<'a, Context> Named<'a, Context> for HttpClientErrorName
where
Context: HasUrlType + HasStatusCodeType,
{
type Type = HttpClientError;
}We would also define a separate CanRaiseNamedError trait, that lets us use the name as the generic parameter:
#[cgp_component(NamedErrorRaiser)]
pub trait CanRaiseNamedError<Name>: HasErrorType {
fn raise_error<'a>(name: &'a Name, e: Name::Type) -> Self::Error
where
Name: Named<'a, Self>
;
}This way, we can provide custom wiring of specific named error without specifying the constraints, such as:
delegate_components! {
Context {
NamedErrorRaiserComponent:
UseDelegate<new NamedErrorHandlers {
HttpClientErrorName:
ToVariant(Symbol!("HttpClient")),
...
}>,
}
}Embedded Error Handler
Similar to #181, we will also implement auto error handlers inside #[cgp_impl] and #[cgp_auto_impl], so that they can be embedded without a separate trait definition using #[cgp_auto_error] macro:
#[cgp_impl]
impl<Context> HttpRequestProvider for Context
where
Context: HasUrlType + HasStatusCodeType + HasErrorType + ...,
{
#[getter]
fn base_url(&self) -> &Self::Url;
#[error("HTTP request to URL {} returned error status code: {}", self.url, self.status_code)]
fn raise_http_client_error(
&self,
#[use(Display)]
url: &Self::Url,
#[use(Display)]
status_code: &Self::StatusCode,
) -> Self::Error;
async fn send_http_request(&self, url: &Self::Url) -> Result<Self::Response, Self::Error> {
...
}
}Behind the scene, the getter and error handler functions will be extracted by #[cgp_impl] to a separate helper blanket trait. And #[cgp_auto_impl] will simply include the implementation within the same trait body.
Conclusion
The new CGP error handling macros will introduce advanced machinery to make it easier to work with abstract error types in CGP. This is crucial for CGP to gain wider adoption, as error handling is a fundamental programming problem that is best solved by CGP.