SmartErr

SmartErr, an error handling library, introduces several convenient aproaches to raise, gather and distibute domain-specific errors in libraries and/or applications.

With SmartErr you'll be able to

Quick overview

See this example below.

Raising errors

Some functions may return simple types instead of Result. This part of the library is devoted to the processing of this kind of results. Simple values are converted with raise (or raise_with) and throw (or throw_with) methods from Throwable trait. raise emits an error if source is NOT treated as failure and throw emits an error if it's already in a failure state. Here is a reference table for types that have an implementation of Throwable trait:

| Source type | throw condition for the error | raise condition | | -------------------------- | ------------------------------- | ----------------- | | numbers (i32, usize, etc) | != 0 | == 0 | | bool | false | true | | strings (&str, String etc) | isempty() | !isempty() | | Option | Some | None | | Result | Ok | Err |

If the condition is not met, the original value will be returned.

Assume there is some numeric input. To convert it into Result using Throwable: ```rust fn rawthrowable(val: i32) -> Result> { val.throw() //val.throwwith("raw error") }

[test]

pub fn testthrowable() { asserteq!(rawthrowable(0).unwrap(), 0); asserteq!(rawthrowable(10).iserr(), true); asserteq!(format!("{}", rawthrowable(10).unwraperr()), "raw error { value: 10 }" ); } ``` To convert with _Erroneous:

```rust smarterr_fledged!(DomainErrors{ DomainError<> -> "Domain error" });

fn rawerroneous(val: i32) -> Result> { val.throwerr(RawError::new_with(val, "raw error")) }

fn rawerroneousthen(val: i32) -> Result> { val.throwthen(|v| RawError::newwith(v, "raw error")) }

fn rawerroneousctx(val: i32) -> Result { val.throw_ctx(DomainErrorCtx{}) }

[test]

pub fn testerroneous() { asserteq!(rawerroneous(0).unwrap(), 0); asserteq!(rawerroneousthen(10).iserr(), true); asserteq!(format!("{}", rawerroneousthen(10).unwraperr()), "raw error { value: 10 }" ); asserteq!(format!("{}", rawerroneousctx(10).unwrap_err()), "Domain error, caused by: raw error { value: 10 }" ); } ``` Domain error processing is described in Defining errors section.

raise alternative could be used instead of throw as well. The only difference is that the raise condition is the opposite of throw.

Defining errors

There are 2 approaches to define errors: * "fledged": domain errors are defined globally (within the selected visibility) * function-based: error set is specific for the each function

Both shares the same sintax, with limited inheritance for the fledged style.

Fledged style

Fledged style is mostly convenient for standalone doman-specific errors. The following example demonstrates the usage of smarterrfledged_ macros which is designed to support fledged approach. rust smarterr_fledged!(pub PlanetsError { MercuryError{} -> "Mercury error", pub MarsError{ind: usize} -> "Mars Error", SaturnError<<i32>> -> "Saturn error", EarthError<ParseIntError> -> "EarthError", }); First it should be defined the name of the error set and (optionally) it's visibility. Then goes certain errors definition inside curly braces. It follows simple pattern: [visibility] name[<[< source error type >]>] [{ context struct }] -> "error message", The following code will be generated under the hood (shown without minor details and cutted to MarsError only):

```rust

[derive(Debug)]

pub enum PlanetsError { MercuryError(MercuryError), MarsError(MarsError), SaturnError(SaturnError), EarthError(EarthError), }

/* cutted: Error and Display implementations for PlanetsError */

[derive(Debug)]

pub struct MarsError { ctx: MarsErrorCtx, }

impl MarsError { pub fn new(src: ES, ctx: MarsErrorCtx) -> Self { MarsError { ctx } } pub fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } pub fn defaultmessage(&self) -> &'static str { "Mars Error" } }

/* cutted: Display implementation for MarsError */

[derive(Debug)]

[allow(dead_code)]

pub struct MarsErrorCtx { ind: usize, }

impl smarterr::IntoError for MercuryErrorCtx { fn intoerror(self, source: ES) -> PlanetsError { PlanetsError::MercuryError(MercuryError::new(source, self)) } } impl smarterr::IntoError for MarsErrorCtx { fn intoerror(self, source: ES) -> PlanetsError { PlanetsError::MarsError(MarsError::new(source, self)) } } impl smarterr::IntoError for SaturnErrorCtx { fn intoerror(self, source: i32) -> PlanetsError { PlanetsError::SaturnError(SaturnError::new(source, self)) } } impl smarterr::IntoError for EarthErrorCtx { fn intoerror(self, source: ParseIntError) -> PlanetsError { PlanetsError::EarthError(EarthError::new(source, self)) } } ```

Several key details for the generated code:

  1. Domain error set is the enum.
  2. For each error (enum value) additional structure is created, its name is the same as the name of the error.
  3. If context has been defined, the corresponding structure will be created. Its name is the error name followed with the Ctx suffix.

The example above it pretty simple and does not demonstate source error definition. Usually you'd like to set up source error. There are several posibilites:

| source | definition example | | ------------- | ---------------------------------------------- | | no source | MercuryError -> "Mercury error" | | dyn Error | MercuryError<> -> "Mercury error" | | certain error | MercuryError<SourceError> -> "Mercury error" | | dyn Debug | MercuryError<<>> -> "Mercury error" | | certain Debug | MercuryError<<i32>> -> "Mercury error" |

Raising errors is pretty simple: rust "z12".parse::<i32>().throw_ctx(EarthErrorCtx{}) Note that it's done with *Ctx structure (EarthErrorCtx in this example) which has an implementation of smarterr::IntoError trait.

Function-based style

This is a common situation when there are several functions calling from each other. Usually each function returns its own error set and some unhandled errors from the called one. Generally it is possible to use one error set (enum) for all functions but that's not quite right. The functions' contracts are inaccurate since they return subset of the common enum and some errors will never happen. If some functions are public it might be a problem to hide unused errors from the internals.

The more precise solution is to define its own error set for each function. But besides being quite difficult, it creates another problem. Some errors may be defined several times for each error set and require mapping between them even that they are the same. SmartErr solves this problem providing all necessary and optimized stuff behind the scenes.

For this, 2 additional keywords were introduced:

Here's how it works:

FBS example

```rust

[smarterr(

AlfaError{ind: i32, ext: String} -> "Alfa error",
BetaError<>{ind: i32} -> "Beta error",
BetaWrappedError<ParseIntError> -> "Beta Wrapped Error",
GammaError<<>>{ext: String} -> "Gamma error",
GammaWrappedError<<i32>>{ext: String} -> "Gamma Wrapped error",

)] pub fn greekfunc(errind: usize) -> String { let okstr = "All is ok".tostring(); let errstr = "Error raised".tostring(); let ext = "ext".tostring(); match errind { 0 => Ok(okstr), 1 => errstr.raisectx(AlfaErrorCtx { ind: -1, ext }), 2 => "z12".parse::().throwctx(BetaErrorCtx { ind: -2 }).map(|| okstr), 3 => "z12".parse::().throwctx(BetaWrappedErrorCtx {}).map(|| okstr), 4 => errstr.raisectx(GammaErrorCtx { ext }), 5 => 5000000.throwctx(GammaWrappedErrorCtx { ext }).map(|| okstr), _ => Ok(ok_str), } }

[smarterr(

from GreekFuncError { 
    AlfaError, BetaError<>, BetaWrappedError<ParseIntError>, GammaError<<>>, 
    handled GammaWrappedError
},
XError{ind: i32, ext: String} -> "X error",
YError{ind: i32} -> "Y error",
pub ZError<<String>>{ind: usize} -> "Z Error",

)] fn latinfunc(errind: usize) { greekfunc(errind).handle(|h| match h { GreekFuncErrorHandled::GammaWrappedError(data) => data.ctx.ext.throwctx(ZErrorCtx { ind: errind }), })?; Ok(()) }

[smarterr(

from GreekFuncError {
    AlfaError -> "Imported Alfa error",
    BetaError<> -> "Imported Beta error",
    BetaWrappedError<std::num::ParseIntError> -> "Imported Beta Wrapped Error",
    handled GammaError,
    handled GammaWrappedError,
},
from LatinFuncError {
    AlfaError, BetaError<>, BetaWrappedError<ParseIntError>, ZError<<String>>, 
    handled { GammaError, XError, YError }
},
FirstError{ind: i32, ext: String} -> "First error",
SecondError{ind: i32} -> "Second error",
ThirdError{} -> "Third Error",

)] pub fn numericfunc(errind: usize) -> String { let g = greekfunc(errind).handle(|h| match h { GreekFuncErrorHandled::GammaWrappedError(e) => e.ctx.ext.clone().raisectx(FirstErrorCtx{ind: errind as i32, ext: e.ctx.ext}), GreekFuncErrorHandled::GammaError(e) => e.ctx.ext.raisectx(SecondErrorCtx{ ind: errind as i32 }), })?;

latin_func(err_ind).handle(|h| match h {
    LatinFuncErrorHandled::XError(e)=>
        ().raise_ctx(FirstErrorCtx{ ind: err_ind as i32, ext: e.ctx.ext }),
    LatinFuncErrorHandled::YError(e)=>
        ().raise_ctx(SecondErrorCtx{ ind: e.ctx.ind }),
    LatinFuncErrorHandled::GammaError(_) => Ok(())
})?;

let t = ().raise_ctx(MarsErrorCtx{ind: err_ind});
t.throw_ctx(BetaErrorCtx{ ind: err_ind as i32 })?;

Ok(g)

} ```