obce-codegen 0.1.0

The library provides tools and primitives to simplify the development of chain extensions for ink! and Substrate.
Documentation
use darling::FromMeta;
use itertools::Itertools;
use proc_macro2::{
    Ident,
    TokenStream,
};
use quote::quote;
use syn::{
    parse2,
    Error,
    Expr,
    ItemEnum,
};

use crate::{
    format_err_spanned,
    types::AttributeArgs,
    utils::AttributeParser,
};

fn default_require_ret_val() -> bool {
    true
}

#[derive(FromMeta)]
struct ErrorAttrs {
    #[darling(default = "default_require_ret_val")]
    require_ret_val: bool,
}

#[derive(FromMeta)]
struct ErrorVariantAttrs {
    critical: Option<()>,
    ret_val: Option<Expr>,
}

struct RetValInfo<'a> {
    variant_name: &'a Ident,
    ret_val: Expr,
}

pub fn generate(attrs: TokenStream, input: TokenStream) -> Result<TokenStream, Error> {
    let mut enum_item: ItemEnum = parse2(input)?;
    let ident = enum_item.ident.clone();
    let enum_attrs = ErrorAttrs::from_list(&syn::parse2::<AttributeArgs>(attrs)?)?;

    let (impl_generics, ty_generics, where_clause) = enum_item.generics.split_for_impl();

    let mut critical_variant = None;

    let mut ret_val_variants = vec![];

    for variant in enum_item.variants.iter_mut() {
        let variant_name = &variant.ident;

        let (obce_attrs, mut other_attrs) = variant.attrs.iter().cloned().split_attrs()?;

        let variant_attrs = ErrorVariantAttrs::from_list(&obce_attrs)?;

        if variant_attrs.critical.is_some() {
            other_attrs.push(syn::parse_quote! {
                #[cfg(feature = "substrate")]
            });

            let previous_critical_variant = critical_variant.replace(quote! {
                #[cfg(feature = "substrate")]
                impl #impl_generics ::obce::substrate::SupportCriticalError for #ident #ty_generics #where_clause {
                    fn try_to_critical(self) -> Result<::obce::substrate::CriticalError, Self> {
                        match self {
                            Self::#variant_name(error) => Ok(error),
                            _ => Err(self)
                        }
                    }
                }
            });

            if let Some(variant) = previous_critical_variant {
                return Err(format_err_spanned!(
                    variant,
                    "only one enum variant can be marked as `#[obce(critical)]`",
                ))
            }
        }

        if let Some(ret_val) = variant_attrs.ret_val {
            ret_val_variants.push(RetValInfo { variant_name, ret_val });
        } else if enum_attrs.require_ret_val && !ret_val_variants.is_empty() {
            return Err(format_err_spanned!(
                variant,
                "you have to mark this variant with `ret_val` or set `require_ret_val` to `false`"
            ))
        }

        variant.attrs = other_attrs;
    }

    if let Some(expr) = ret_val_variants.iter().map(|info| &info.ret_val).duplicates().next() {
        return Err(format_err_spanned!(expr, "ret_val value is used twice"))
    }

    let formatted_ret_val = ret_val_variants.iter().map(|RetValInfo { variant_name, ret_val }| {
        quote! {
            #ident::#variant_name => Ok(Self::Converging(#ret_val)),
        }
    });

    let ret_val_impl = quote! {
        impl #impl_generics ::core::convert::TryFrom<#ident #ty_generics>
            for ::obce::substrate::pallet_contracts::chain_extension::RetVal
            #where_clause
        {
            type Error = #ident #ty_generics;

            fn try_from(value: #ident #ty_generics) -> Result<Self, #ident #ty_generics> {
                match value {
                    #(#formatted_ret_val)*
                    _ => Err(value)
                }
            }
        }
    };

    Ok(quote! {
        #[derive(Debug, Copy, Clone, PartialEq, Eq, ::scale::Encode, ::scale::Decode)]
        #[cfg_attr(feature = "std", derive(::scale_info::TypeInfo))]
        #enum_item

        #critical_variant

        #[cfg(feature = "substrate")]
        #ret_val_impl
    })
}