tuple_for_each 0.1.0

Provides macros and methods to iterate over the fields of a tuple struct
Documentation
#![doc = include_str!("../README.md")]

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{Data, DataStruct, DeriveInput, Error, Fields, Index};

/// Provides macros and methods to iterate over the fields of a tuple struct.
///
/// See the [crate-level documentation](crate).
#[proc_macro_derive(TupleForEach)]
pub fn tuple_for_each(item: TokenStream) -> TokenStream {
    let ast = syn::parse_macro_input!(item as DeriveInput);
    if let Data::Struct(strct) = &ast.data {
        if let Fields::Unnamed(_) = strct.fields {
            return impl_for_each(&ast, strct).into();
        }
    }
    Error::new_spanned(
        ast,
        "attribute `tuple_for_each` can only be attached to tuple structs",
    )
    .to_compile_error()
    .into()
}

fn gen_doc(kind: &str, tuple_name: &str, macro_name: &str) -> String {
    match kind {
        "for_each" => format!(
            "
            Iterates over the fields of a instance of [`{tuple_name}`].

            This macro is generated by [`#[derive(tuple_for_each::TupleForEach)]`](tuple_for_each).
            # Examples
            ```ignore
            let tuple: {tuple_name};
            {macro_name}_for_each!(field in tuple {{
                // do something with `field`
            }});
            ```"
        ),
        "enumerate" => format!(
            "
            Iterates over the fields of a instance of [`{tuple_name}`], with the field index.

            This macro is generated by [`#[derive(tuple_for_each::TupleForEach)]`](tuple_for_each).
            # Examples
            ```ignore
            let tuple: {tuple_name};
            {macro_name}_enumerate!((i, field) in tuple {{
                // do something with the index `i` and `field`
            }});
            ```"
        ),
        _ => unimplemented!(),
    }
}

fn impl_for_each(ast: &DeriveInput, strct: &DataStruct) -> proc_macro2::TokenStream {
    let tuple_name = &ast.ident;
    let macro_name = pascal_to_snake(tuple_name.to_string());
    let macro_for_each = format_ident!("{}_for_each", macro_name);
    let macro_enumerate = format_ident!("{}_enumerate", macro_name);

    let field_num = strct.fields.len();
    let mut for_each = vec![];
    let mut for_each_mut = vec![];
    let mut enumerate = vec![];
    let mut enumerate_mut = vec![];
    for i in 0..field_num {
        let idx = Index::from(i);
        for_each.push(quote!( { let $item = &$tuple.#idx; $code } ));
        for_each_mut.push(quote!( { let $item = &mut $tuple.#idx; $code } ));
        enumerate.push(quote!({
            let $idx = #idx;
            let $item = &$tuple.#idx;
            $code
        }));
        enumerate_mut.push(quote!({
            let $idx = #idx;
            let $item = &mut $tuple.#idx;
            $code
        }));
    }

    let macro_for_each_doc = gen_doc("for_each", &tuple_name.to_string(), &macro_name);
    let macro_enumerate_doc = gen_doc("enumerate", &tuple_name.to_string(), &macro_name);
    quote! {
        impl #tuple_name {
            /// Number of items in the tuple.
            pub const fn len(&self) -> usize {
                #field_num
            }

            /// Whether the tuple has no field.
            pub const fn is_empty(&self) -> bool {
                self.len() == 0
            }
        }

        #[doc = #macro_for_each_doc]
        #[macro_export]
        macro_rules! #macro_for_each {
            ($item:ident in $tuple:ident $code:block) => {
                #(#for_each)*
            };
            ($item:ident in mut $tuple:ident $code:block) => {
                #(#for_each_mut)*
            };
        }

        #[doc = #macro_enumerate_doc]
        #[macro_export]
        macro_rules! #macro_enumerate {
            (($idx:ident, $item:ident) in $tuple:ident $code:block) => {
                #(#enumerate)*
            };
            (($idx:ident, $item:ident) in mut $tuple:ident $code:block) => {
                #(#enumerate_mut)*
            };
        }
    }
}

fn pascal_to_snake(pascal: String) -> String {
    let mut ret = String::new();
    for ch in pascal.chars() {
        if ch.is_ascii_uppercase() && !ret.is_empty() {
            ret.push('_')
        }
        ret.push(ch.to_ascii_lowercase());
    }
    ret
}