enum_delegate_lib 0.2.0

Internal macro implementations for enum_delegate - use to implement your own macros
Documentation
use proc_macro2::TokenStream;
use quote::{format_ident, quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{parse2, ItemTrait, TraitItem};

use crate::identifiers::my_attribute_ident;

/// The `register` macro implementation.
///
/// See the `enum_delegate` crate for more information.
// we're mirroring how macro signatures look
#[allow(clippy::needless_pass_by_value)]
#[must_use]
pub fn register(attribute_args: TokenStream, item: TokenStream) -> TokenStream {
    if !attribute_args.is_empty() {
        let span = attribute_args.span();
        return quote_spanned!(span => compile_error!("this macro doesn't accept any arguments"););
    }

    let parsed_trait: ItemTrait = match parse2(item) {
        Ok(parsed) => parsed,
        Err(error) => return error.to_compile_error(),
    };
    let cleaned_trait = with_helper_attributes_removed(&parsed_trait);

    // Since this macro is `[macro_export]`ed, it will end up at the crate root, and therefore needs a unique name among its peers.
    // We want to allow multiple traits with the same name (in different modules), so we cannot make a unique name from the trait name alone.
    // We can't get the module path. We can use the file path, but we'd need to clean it up so that it is a valid identifier. Also span source_file is unstable.
    // A "hack" is to generate a random id and append it to our name.

    let random_id = random_hopefully_unique_id();
    let macro_name = format_ident!("enum_delegate_{}_{}", &parsed_trait.ident, random_id);

    let trait_name = &parsed_trait.ident;

    quote! {
        #[doc(hidden)]
        #[macro_export]
        macro_rules! #macro_name {
            ($trait_path: path, $enum_path: path, $enum_declaration: item) => {
                enum_delegate::implement_trait_for_enum!{
                    $trait_path,
                    #parsed_trait,
                    $enum_path,
                    $enum_declaration
                }
            };
        }

        #[doc(hidden)]
        pub use #macro_name as #trait_name;

        #cleaned_trait
    }
}

/// Generate a random alphanumerical ID that is extremely likely (read: practically guaranteed) to be unique
fn random_hopefully_unique_id() -> String {
    use rand::distributions::Alphanumeric;
    use rand::Rng;

    // we want to generate a unique ID using the characters {a-z, A-Z, 0-1}
    // 26+26+10 = 62 available characters
    // how many bit randomness do we want? idk..
    // say we want the same collision resistance as SHA-256
    // which has a 256-bit hash
    // if its good enough for NSA is good enough for me right?
    // anyway, log_62(2^256) is like 42.99
    // so that means we need like 43 characters. (i hope i'm doing this right?)
    // however, 42 is close, and an objectively better number.
    // what's that? you say it's more likely to collide with 42 characters?
    // i bet you $5 it won't.

    rand::thread_rng()
        .sample_iter(&Alphanumeric)
        .take(42)
        .map(char::from)
        .collect()
}

/// Generate a clone of the specified trait, but with all `enum_delegate` metadata removed, so that it compiles
///
/// (Since `enum_delegate` is not a valid attribute)
fn with_helper_attributes_removed(original: &ItemTrait) -> ItemTrait {
    let mut cleaned = original.clone();

    cleaned.items.iter_mut().for_each(|item| {
        if let TraitItem::Type(t) = item {
            // todo: clean up when drain_filter is stable

            let attributes = &mut t.attrs;

            let mut i = 0;
            while i < attributes.len() {
                if attributes[i].path.is_ident(&my_attribute_ident()) {
                    let _ = attributes.remove(i);
                } else {
                    i += 1;
                }
            }
        }
    });

    cleaned
}