throwing-macros 0.1.1

Create explicit errors easily with a handy macro
Documentation
use crate::types::{CompositeError, Variant};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{parse_quote, ItemFn, ReturnType, Type};

fn error_enum(error: &CompositeError) -> TokenStream {
    let CompositeError {
        visibility,
        name,
        variants,
        ..
    } = error;

    let variants = variants
        .iter()
        .map(|Variant { name, typ }| quote!(#name(#typ)));

    quote!(
        #[derive(::core::fmt::Debug)]
        #visibility enum #name {
            #(#variants),*
        }
    )
}

fn impl_from_variant(error: &CompositeError, variant: &Variant) -> TokenStream {
    let error_name = &error.name;
    let Variant { typ, name } = variant;

    quote!(
        #[automatically_derived]
        impl ::core::convert::From<#typ> for #error_name {
            fn from(value: #typ) -> #error_name {
                #error_name::#name(value)
            }
        }
    )
}

fn impl_from_composed(error: &CompositeError, typ: &Type) -> TokenStream {
    let error_name = &error.name;

    quote!(
        #[automatically_derived]
        impl ::core::convert::From<#typ> for #error_name {
            fn from(value: #typ) -> #error_name {
                ::throwing::SubError::to_super_error(value)
            }
        }
    )
}

fn impl_sub_error(error: &CompositeError) -> TokenStream {
    let CompositeError { name, variants, .. } = error;

    let froms = variants
        .iter()
        .map(|Variant { typ, .. }| quote!(::core::convert::From<#typ>));

    let arms = variants.iter().map(
        |Variant { name: variant, .. }| quote!(#name::#variant(e) => ::core::convert::From::from(e)),
    );

    quote!(
        #[automatically_derived]
        impl<T> ::throwing::SubError<T> for #name where T: #(#froms)+* {
            fn to_super_error(self) -> T {
                match self {
                    #(#arms),*
                }
            }
        }
    )
}

fn impl_display(error: &CompositeError) -> TokenStream {
    let CompositeError { name, variants, .. } = error;

    let body = if variants.is_empty() {
        quote!(match *self {})
    } else {
        let arms = variants
            .iter()
            .map(|Variant { name: variant, .. }| quote!(#name::#variant(e) => ::core::fmt::Display::fmt(e, f)));

        quote!(
            match self {
                #(#arms),*
            }
        )
    };

    quote!(
        #[automatically_derived]
        impl ::core::fmt::Display for #name {
            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
                #body
            }
        }
    )
}

fn impl_error(error: &CompositeError) -> TokenStream {
    let CompositeError { name, variants, .. } = error;

    let body = if variants.is_empty() {
        quote!(match *self {})
    } else {
        let arms = variants
            .iter()
            .map(|Variant { name: variant, .. }| quote!(#name::#variant(e) => ::core::option::Option::Some(e)));

        quote!(
            match self {
                #(#arms),*
            }
        )
    };

    quote!(
        #[automatically_derived]
        impl ::std::error::Error for #name {
            fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> {
                #body
            }
        }
    )
}

pub fn error_definition(error: CompositeError) -> TokenStream {
    let mut stream = error_enum(&error);

    for variant in &error.variants {
        stream.extend(impl_from_variant(&error, variant));
    }

    for typ in &error.composed {
        stream.extend(impl_from_composed(&error, typ));
    }

    stream.extend(impl_sub_error(&error));
    stream.extend(impl_display(&error));
    stream.extend(impl_error(&error));

    stream
}

fn wrap_return_with_result(ret: ReturnType, error: Ident) -> ReturnType {
    let (arrow, typ) = match ret {
        ReturnType::Default => (Default::default(), parse_quote!(())),
        ReturnType::Type(arrow, typ) => (arrow, *typ),
    };

    let typ = parse_quote!(::core::result::Result<#typ, #error>);

    ReturnType::Type(arrow, Box::new(typ))
}

pub fn patch_function(mut function: ItemFn, error: Ident) -> ItemFn {
    function.sig.output = wrap_return_with_result(function.sig.output, error);

    function
}