fuels-macros 0.77.0

Fuel Rust SDK macros to generate types from ABI.
Documentation
pub(crate) use command::Command;
use itertools::{Itertools, chain};
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, quote};
use syn::{
    Attribute, DataEnum, DataStruct, Error, Fields, GenericParam, Generics, TypeParam, Variant,
};
pub(crate) use unique_lit_strs::UniqueLitStrs;
pub(crate) use unique_name_values::UniqueNameValues;

mod command;
mod unique_lit_strs;
mod unique_name_values;

pub(crate) trait ErrorsExt: Iterator<Item = Error> + Sized {
    fn combine_errors(self) -> Option<Self::Item>;
    fn validate_no_errors(self) -> Result<(), Self::Item>;
}

impl<T> ErrorsExt for T
where
    T: Iterator<Item = Error> + Sized,
{
    fn combine_errors(self) -> Option<Self::Item> {
        self.reduce(|mut errors, error| {
            errors.combine(error);
            errors
        })
    }

    fn validate_no_errors(self) -> Result<(), Self::Item> {
        if let Some(err) = self.combine_errors() {
            Err(err)
        } else {
            Ok(())
        }
    }
}

fn generate_duplicate_error<T>(duplicates: &[&T]) -> Error
where
    T: ToTokens,
{
    let mut iter = duplicates.iter();

    let original_error = iter
        .next()
        .map(|first_el| Error::new_spanned(first_el, "original defined here:"));

    let the_rest = iter.map(|duplicate| Error::new_spanned(duplicate, "duplicate!"));

    chain!(original_error, the_rest)
        .combine_errors()
        .expect("has to be at least one error!")
}

fn group_up_duplicates<T, K, KeyFn>(name_values: &[T], key: KeyFn) -> Vec<Vec<&T>>
where
    KeyFn: Fn(&&T) -> K,
    K: Ord,
{
    name_values
        .iter()
        .sorted_by_key(&key)
        .group_by(&key)
        .into_iter()
        .filter_map(|(_, group)| {
            let group = group.collect::<Vec<_>>();

            (group.len() > 1).then_some(group)
        })
        .collect()
}

fn validate_no_duplicates<T, K, KeyFn>(elements: &[T], key_fn: KeyFn) -> syn::Result<()>
where
    KeyFn: Fn(&&T) -> K + Copy,
    T: ToTokens,
    K: Ord,
{
    group_up_duplicates(elements, key_fn)
        .into_iter()
        .map(|duplicates| generate_duplicate_error(&duplicates))
        .validate_no_errors()
}

pub fn validate_and_extract_generic_types(generics: &Generics) -> syn::Result<Vec<&TypeParam>> {
    generics
        .params
        .iter()
        .map(|generic_param| match generic_param {
            GenericParam::Type(generic_type) => Ok(generic_type),
            GenericParam::Lifetime(lifetime) => {
                Err(Error::new_spanned(lifetime, "Lifetimes not supported"))
            }
            GenericParam::Const(const_generic) => Err(Error::new_spanned(
                const_generic,
                "Const generics not supported",
            )),
        })
        .collect()
}

enum Member {
    Normal { name: Ident, ty: TokenStream },
    Ignored { name: Ident },
}

pub(crate) struct Members {
    members: Vec<Member>,
    fuels_core_path: TokenStream,
}

impl Members {
    pub(crate) fn from_struct(
        fields: DataStruct,
        fuels_core_path: TokenStream,
    ) -> syn::Result<Self> {
        let named_fields = match fields.fields {
            Fields::Named(named_fields) => Ok(named_fields.named),
            Fields::Unnamed(fields) => Err(Error::new_spanned(
                fields.unnamed,
                "Tuple-like structs not supported",
            )),
            _ => {
                panic!("This cannot happen in valid Rust code. Fields::Unit only appears in enums")
            }
        }?;

        let members = named_fields
            .into_iter()
            .map(|field| {
                let name = field
                    .ident
                    .expect("`FieldsNamed` to only contain named fields");
                if has_ignore_attr(&field.attrs) {
                    Member::Ignored { name }
                } else {
                    let ty = field.ty.into_token_stream();
                    Member::Normal { name, ty }
                }
            })
            .collect();

        Ok(Members {
            members,
            fuels_core_path,
        })
    }

    pub(crate) fn from_enum(data: DataEnum, fuels_core_path: TokenStream) -> syn::Result<Self> {
        let members = data
            .variants
            .into_iter()
            .map(|variant: Variant| {
                let name = variant.ident;
                if has_ignore_attr(&variant.attrs) {
                    Ok(Member::Ignored { name })
                } else {
                    let ty = match variant.fields {
                        Fields::Unnamed(fields_unnamed) => {
                            if fields_unnamed.unnamed.len() != 1 {
                                return Err(Error::new(
                                    fields_unnamed.paren_token.span.join(),
                                    "must have exactly one element",
                                ));
                            }
                            fields_unnamed.unnamed.into_iter().next()
                        }
                        Fields::Unit => None,
                        Fields::Named(named_fields) => {
                            return Err(Error::new_spanned(
                                named_fields,
                                "struct-like enum variants are not supported",
                            ));
                        }
                    }
                    .map(|field| field.ty.into_token_stream())
                    .unwrap_or_else(|| quote! {()});
                    Ok(Member::Normal { name, ty })
                }
            })
            .collect::<Result<Vec<_>, _>>()?;

        Ok(Members {
            members,
            fuels_core_path,
        })
    }

    pub(crate) fn names(&self) -> impl Iterator<Item = &Ident> + '_ {
        self.members.iter().filter_map(|member| {
            if let Member::Normal { name, .. } = member {
                Some(name)
            } else {
                None
            }
        })
    }

    pub(crate) fn names_as_strings(&self) -> impl Iterator<Item = TokenStream> + '_ {
        self.names().map(|ident| {
            let name = ident.to_string();
            quote! {#name.to_string()}
        })
    }

    pub(crate) fn ignored_names(&self) -> impl Iterator<Item = &Ident> + '_ {
        self.members.iter().filter_map(|member| {
            if let Member::Ignored { name } = member {
                Some(name)
            } else {
                None
            }
        })
    }

    pub(crate) fn param_type_calls(&self) -> impl Iterator<Item = TokenStream> + '_ {
        let fuels_core_path = self.fuels_core_path.to_token_stream();
        self.members.iter().filter_map(move |member| match member {
            Member::Normal { ty, .. } => {
                Some(quote! { <#ty as #fuels_core_path::traits::Parameterize>::param_type() })
            }
            _ => None,
        })
    }
}

pub(crate) fn has_ignore_attr(attrs: &[Attribute]) -> bool {
    attrs.iter().any(|attr| match &attr.meta {
        syn::Meta::Path(path) => path.get_ident().is_some_and(|ident| ident == "Ignore"),
        _ => false,
    })
}