skerry 0.1.7

Super Kool ERRors Yoh - A type-safe, zero-boilerplate error management framework.
Documentation

Skerry: Super Kool ERRors Yoh

Example:

use skerry::*;

// There can be only one #[sherry_mod] in your project
#[skerry_mod]
pub mod errors {
    pub struct DatabaseError;
    pub struct AuthError;
    pub struct ValidationError;

    pub struct InvalidParse;

    #[from]
    pub struct LibError(ErrorFromLib);
}

// Generates a CheckAuthError enum automatically
#[skerry_fn]
fn check_auth() -> Result<(), e![AuthError]> {
    Err(CheckAuthError::AuthError(AuthError))
}


struct Controller;

#[skerry_impl(prefix(Controller))] // This allows #[skerry_fn] to run on impl blocks
impl Controller {
    // Use '*' to expand and bubble up sub-errors seamlessly.
    #[skerry_fn]
    pub fn run() -> Result<(), e![LibError, *CheckAuthError]> {
        // AuthError is pulled in from check_auth via '*CheckAuthError'
        check_auth()?;

        // Automatically bubble up library errors as long as an
        // error from `#[skerry_mod]` implements `From` for it.
        lib_fn_that_returns_error()?;

        Ok(())
    }
}
#[skerry_trait]
trait ToJson {
    #[skerry_fn]
    fn to_json(&self) -> Result<(), e![InvalidParse]>;
}

#[skerry_impl]
impl ToJson for Controller {
    // Whenever you do not want to generate a new error just don't
    // use e![], this will instead reuse an existing error
    #[skerry_fn]
    fn to_json(&self) -> Result<(), ToJsonError> {
        Ok(())
    }
}

// Manually define composite errors like this
define_error!(ManualDefine, [ValidationError, *ToJsonError]);

Core Workflow

  • Define all possible error structs in a #[skerry_mod].
  • Mark functions with #[skerry_fn].
  • Use the * operator to bubble up errors from sub-functions without manually mapping variants.

The Error Module

Every project needs one module (usually errors.rs) that acts as the source of truth.

// Recommended to be pub for easier macro expansions
pub use skerry::*;

#[skerry_mod]
mod errors {
    pub struct ErrA;
    pub struct ErrB;
    pub struct ErrC;
    pub struct DatabaseErr;
    #[from]
    pub struct OuterLibError(LibError);
}

You can also anotate with #[from] to automatically add conversions from the inner type. This is only valid for tuple structs with a single element.

Note: When using errors in any other file, import them via crate::errors::*; instead of individual imports to ensure the macros can resolve the paths correctly.


Function-Specific Enums

By using #[skerry_fn], you define a return type using a tuple of error structs. Skerry transforms this into a unique enum named {FunctionName}Error.

#[skerry_fn]
pub fn low_level() -> Result<(), e![ErrA, ErrB]> {
    // Generates LowLevelError { ErrA(ErrA), ErrB(ErrB) }
    Err(LowLevelError::ErrA(ErrA))
    // You can also type Err(ErrA.into())
}

The Asterisk (*) Expansion

When you put *OtherFnError in your return array it pulls all variants from OtherFnError into your current function's list.

  • Deduplication: Variants are deduplicated automatically. If ErrA is added manually and also exists inside a * expansion, only one variant is generated.
#[skerry_fn]
pub fn high_level() -> Result<(), e![ErrC, *LowLevelError]> {
    // Sees ErrC -> Adds variant
    // Sees *LowLevelError -> Inspects LowLevelError, finds (ErrA, ErrB)
    // Final HighLevelError contains variants: ErrA, ErrB, ErrC

    low_level()?; // Bubbles up automatically
    Ok(())
}

The syntax below has the exact same effects, *LowLevelError is nothing more than syntatic sugar

#[skerry_fn]
pub fn high_level() -> Result<(), e![ErrA, ErrB, ErrC]> {
    // ...
}

In the cases above the generated enum looks like this

pub enum HighLevelError {
    ErrA(ErrA),
    ErrB(ErrB),
    ErrC(ErrC),
}

Using Skerry inside Impl Blocks

Skerry provides the #[skerry_impl] attribute to handle methods within impl blocks. This attribute coordinates with #[skerry_fn] to split the generated code so error enums are generated outside the impl block.

Example

pub struct Database;

#[skerry_impl(prefix(Database))] // Optional prefix for functions inside impl block
impl Database {
    #[skerry_fn]
    pub fn connect(&self) -> Result<(), e![*RemoteCallError]> {
        remote_call()?;
        Ok(())
    }
}

fn main() {
    let db = Database;
    let result: Result<(), DatabaseConnectError> = db.connect();
    assert!(result.is_ok());
}

Using Skerry inside Trait Blocks

Skerry provides the #[skerry_trait] attribute to handle methods within trait blocks. This attribute coordinates with #[skerry_fn] to split the generated code so error enums are generated outside the trait block.

Example

#[skerry_trait(prefix(ToJson))] // Optional prefix for functions inside trait block
trait ToJson {
    #[skerry_fn]
    fn parse(&self) -> Result<(), e![ParseFailed]>;
}

Manual Error Definitions

Manually define composite errors using the define_error! macro. This allows you to skip needing a function to define errors.

Example

define_error!(ManualDefine, [ErrorA, ErrorB]);

#[skerry_fn]
fn my_func_with_custom_error() -> Result<(), ManualDefine> {
    Ok(())
}

Custom Result Feature

The custom_result feature implements a specialized result type and leverages the unstable features to enable even more automation.

Nightly Features Required

To use the full suite of automation provided by this feature, you must enable the following nightly features in your crate root:

#![feature(try_trait_v2)]
#![feature(custom_inner_attributes)]
#![feature(proc_macro_hygiene)]

Overview

Enabling custom_result changes the behavior of the ? operator to support automatic conversion into GlobalErrors<I>.

Feature Gate Effect
try_trait_v2 Allows using ? with custom Result types; removes the strict requirement for #[skerry_fn] on standard functions.
custom_inner_attributes/proc_macro_hygiene Enables the use of #![skerry] at the top of a file to annotate all contents automatically.

Error Declaration

Use #![skerry_mod] to define concise error sets. The #[from] attribute allows automatic wrapping of external library errors.

//! errors.rs
#![skerry_mod]

pub struct ErrA;
pub struct ErrB;

#[from]
pub struct Outer(OuterErrorFromLib);

Comparison

Standard Manual Approach

Traditionally, every function and implementation block requires explicit tagging:

#[skerry_fn]
fn check_auth() -> Result<(), e![AuthError]> {
    Err(CheckAuthError::AuthError(AuthError))
}

struct Controller;

#[skerry_impl(prefix(Controller))]
impl Controller {
    #[skerry_fn]
    pub fn run() -> Result<(), e![LibError, *CheckAuthError]> {
        check_auth()?;
        lib_fn_that_returns_error()?;
        Ok(())
    }
}
#[skerry_trait(prefix(ToJson))]
trait ToJson {
    #[skerry_fn]
    fn to_json(&self) -> Result<(), e![InvalidParse]>;
}

impl ToJson for Controller {
    #[skerry_fn]
    fn to_json(&self) -> Result<(), ToJsonError> {
        Ok(())
    }
}

Automated Approach with custom_result

By adding #![skerry] to the top of your module, the boilerplate is handled automatically.

#![skerry]

fn check_auth() -> Result<(), e![AuthError]> {
    Err(CheckAuthError::AuthError(AuthError))
}

struct Controller;

impl Controller {
    pub fn run() -> Result<(), e![LibError, *CheckAuthError]> {
        check_auth()?;
        lib_fn_that_returns_error()?;
        Ok(())
    }
}

trait ToJson {
    fn to_json(&self) -> Result<(), e![InvalidParse]>;
}

impl ToJson for Controller {
    fn to_json(&self) -> Result<(), ToJsonError> {
        Ok(())
    }
}

Compile-Time Safety

Skerry uses a custom trait system (MissingConvert) to verify error bounds at compile-time. If you try to use ? on a function whose errors are not represented in your current return tuple, the compiler will refuse to build.

License: MIT