fack 0.2.0

Declarative error handling library with no_std support and composable code generation
Documentation
use std::error::Error as _;

use fack::prelude::*;

#[derive(Debug, Error)]
#[error("inner failure")]
struct Inner;

#[derive(Debug, Error)]
#[error("outer failure")]
#[error(source(inner))]
struct StructSource {
    inner: Inner,
}

#[derive(Debug, Error)]
enum EnumSource {
    #[error("named failure")]
    #[error(source(inner))]
    Named { inner: Inner, _code: u8 },

    #[error("tuple failure")]
    #[error(source(0))]
    Tuple(Inner, ()),
}

#[test]
fn structure_source_is_selected_field() {
    let error = StructSource { inner: Inner };
    let source = error.source().expect("declared source must exist");

    assert_eq!(source.to_string(), "inner failure");
}

#[test]
fn enum_sources_are_selected_bindings() {
    let named = EnumSource::Named { inner: Inner, _code: 7 };
    let tuple = EnumSource::Tuple(Inner, ());

    let named_source = named.source().expect("named source must exist");
    let tuple_source = tuple.source().expect("tuple source must exist");

    assert_eq!(named_source.to_string(), "inner failure");
    assert_eq!(tuple_source.to_string(), "inner failure");
}

#[derive(Debug, Error)]
#[error("from struct")]
#[error(from)]
struct StructFrom(Inner);

#[derive(Debug, Error)]
enum EnumFrom {
    #[error("from enum")]
    #[error(from)]
    Inner(Inner),
}

#[test]
fn from_implies_the_converted_field_is_the_source() {
    let structure = StructFrom::from(Inner);
    let enumeration = EnumFrom::from(Inner);

    assert_eq!(structure.source().expect("from source must exist").to_string(), "inner failure");
    assert_eq!(enumeration.source().expect("from source must exist").to_string(), "inner failure");
}

#[derive(Debug, Error)]
#[error(transparent(0))]
struct Transparent(Inner);

#[test]
fn transparent_forwards_display_and_inner_source() {
    let error = Transparent(Inner);

    assert_eq!(error.to_string(), "inner failure");
    assert!(error.source().is_none());
}

#[derive(Debug, Error)]
#[error("tuple {0} debug {1:?} hex {2:#x}")]
struct TupleFormat(u8, &'static str, u16);

#[derive(Debug, Error)]
#[error("named {name} debug {value:?} hex {code:#x}")]
struct NamedFormat {
    name: &'static str,
    value: Option<u8>,
    code: u16,
}

#[test]
fn format_strings_capture_tuple_and_named_fields() {
    let tuple = TupleFormat(7, "text", 0x2a);
    let named = NamedFormat {
        name: "item",
        value: Some(3),
        code: 0x2a,
    };

    assert_eq!(tuple.to_string(), "tuple 7 debug \"text\" hex 0x2a");
    assert_eq!(named.to_string(), "named item debug Some(3) hex 0x2a");
}

#[derive(Debug, Error)]
#[error("escaped {{field}} and {}", value.abs())]
struct ExpressionFormat {
    value: i8,
}

#[test]
fn format_strings_preserve_explicit_arguments_and_escaped_braces() {
    let error = ExpressionFormat { value: -7 };

    assert_eq!(error.to_string(), "escaped {field} and 7");
}

#[derive(Debug, Error)]
#[error("generic {0}")]
struct GenericDisplay<T>(T);

#[derive(Debug, Error)]
#[error("generic debug {0:?}")]
struct GenericDebug<T>(T);

#[test]
fn format_captures_add_only_required_format_bounds() {
    let display = GenericDisplay(17_u8);
    let debug = GenericDebug(Some(4_u8));

    assert_eq!(display.to_string(), "generic 17");
    assert_eq!(debug.to_string(), "generic debug Some(4)");
}

#[derive(Debug, Error)]
#[error("boxed source")]
#[error(source(inner))]
struct BoxedSource {
    inner: Box<Inner>,
}

#[derive(Debug, Error)]
#[error("optional source")]
#[error(source(inner))]
struct OptionalSource {
    inner: Option<Inner>,
}

#[derive(Debug, Error)]
#[error("optional boxed source")]
#[error(source(inner))]
struct OptionalBoxedSource {
    inner: Option<Box<Inner>>,
}

#[derive(Debug, Error)]
#[error("dynamic source")]
#[error(source(inner))]
struct DynamicSource {
    inner: Box<dyn std::error::Error + Send + Sync>,
}

#[test]
fn common_source_container_shapes_are_supported() {
    let boxed = BoxedSource { inner: Box::new(Inner) };
    let optional = OptionalSource { inner: Some(Inner) };
    let absent = OptionalSource { inner: None };
    let optional_boxed = OptionalBoxedSource {
        inner: Some(Box::new(Inner)),
    };
    let dynamic = DynamicSource { inner: Box::new(Inner) };

    assert_eq!(boxed.source().expect("boxed source").to_string(), "inner failure");
    assert_eq!(optional.source().expect("optional source").to_string(), "inner failure");
    assert!(absent.source().is_none());
    assert_eq!(optional_boxed.source().expect("optional boxed source").to_string(), "inner failure");
    assert_eq!(dynamic.source().expect("dynamic source").to_string(), "inner failure");
}

#[derive(Debug, Error)]
#[error("named argument {rendered}", rendered = value.abs())]
struct NamedArgument {
    value: i8,
}

#[test]
fn explicit_named_format_arguments_take_precedence_over_field_capture() {
    let error = NamedArgument { value: -9 };

    assert_eq!(error.to_string(), "named argument 9");
}

fn custom_format(error: &CustomDisplay, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    let CustomDisplay { value } = error;

    write!(formatter, "custom {value}")
}

#[derive(Debug, Error)]
#[error(display(custom_format))]
struct CustomDisplay {
    value: u8,
}

#[test]
fn custom_display_formatter_is_an_escape_hatch() {
    assert_eq!(CustomDisplay { value: 5 }.to_string(), "custom 5");
}

#[derive(Debug)]
struct DebugOnly;

#[derive(Debug, Error)]
#[error("selected {selected}")]
struct SelectiveFormat<T, U> {
    selected: T,
    _unused: U,
}

#[test]
fn unused_fields_do_not_acquire_format_bounds() {
    let error = SelectiveFormat {
        selected: 3_u8,
        _unused: DebugOnly,
    };

    assert_eq!(error.to_string(), "selected 3");
}

#[derive(Debug, Error)]
#[error("named {value:>width$}")]
struct NamedWidth {
    value: u8,
    width: usize,
}

#[derive(Debug, Error)]
#[error("tuple {0:>1$}")]
struct TupleWidth(u8, usize);

#[test]
fn dynamic_width_fields_are_resolved_and_bound() {
    assert_eq!(NamedWidth { value: 7, width: 3 }.to_string(), "named   7");
    assert_eq!(TupleWidth(7, 3).to_string(), "tuple   7");
}

#[derive(Debug, Error)]
#[error("generic from")]
#[error(from)]
struct GenericFrom<E>(E);

#[test]
fn generic_from_adds_the_source_bound_to_only_the_error_impl() {
    let error = GenericFrom::from(Inner);

    assert_eq!(error.source().expect("generic source").to_string(), "inner failure");
}

fn format_custom_enum(error: &CustomEnum, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match error {
        CustomEnum::Value(value) => write!(formatter, "enum custom {value}"),
    }
}

#[derive(Debug, Error)]
enum CustomEnum {
    #[error(display(format_custom_enum))]
    Value(u8),
}

#[test]
fn custom_display_formatter_works_for_enum_variants() {
    assert_eq!(CustomEnum::Value(8).to_string(), "enum custom 8");
}